Cache.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use DirectoryIterator;
  10. use \Doctrine\Common\Cache as DoctrineCache;
  11. use Exception;
  12. use Grav\Common\Config\Config;
  13. use Grav\Common\Filesystem\Folder;
  14. use Grav\Common\Scheduler\Scheduler;
  15. use LogicException;
  16. use Psr\SimpleCache\CacheInterface;
  17. use RocketTheme\Toolbox\Event\Event;
  18. use Symfony\Component\EventDispatcher\EventDispatcher;
  19. use function dirname;
  20. use function extension_loaded;
  21. use function function_exists;
  22. use function in_array;
  23. use function is_array;
  24. /**
  25. * The GravCache object is used throughout Grav to store and retrieve cached data.
  26. * It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
  27. *
  28. * APCu
  29. * RedisCache
  30. * MemCache
  31. * MemCacheD
  32. * FileSystem
  33. */
  34. class Cache extends Getters
  35. {
  36. /** @var string Cache key. */
  37. protected $key;
  38. /** @var int */
  39. protected $lifetime;
  40. /** @var int */
  41. protected $now;
  42. /** @var Config $config */
  43. protected $config;
  44. /** @var DoctrineCache\CacheProvider */
  45. protected $driver;
  46. /** @var CacheInterface */
  47. protected $simpleCache;
  48. /** @var string */
  49. protected $driver_name;
  50. /** @var string */
  51. protected $driver_setting;
  52. /** @var bool */
  53. protected $enabled;
  54. /** @var string */
  55. protected $cache_dir;
  56. protected static $standard_remove = [
  57. 'cache://twig/',
  58. 'cache://doctrine/',
  59. 'cache://compiled/',
  60. 'cache://clockwork/',
  61. 'cache://validated-',
  62. 'cache://images',
  63. 'asset://',
  64. ];
  65. protected static $standard_remove_no_images = [
  66. 'cache://twig/',
  67. 'cache://doctrine/',
  68. 'cache://compiled/',
  69. 'cache://clockwork/',
  70. 'cache://validated-',
  71. 'asset://',
  72. ];
  73. protected static $all_remove = [
  74. 'cache://',
  75. 'cache://images',
  76. 'asset://',
  77. 'tmp://'
  78. ];
  79. protected static $assets_remove = [
  80. 'asset://'
  81. ];
  82. protected static $images_remove = [
  83. 'cache://images'
  84. ];
  85. protected static $cache_remove = [
  86. 'cache://'
  87. ];
  88. protected static $tmp_remove = [
  89. 'tmp://'
  90. ];
  91. /**
  92. * Constructor
  93. *
  94. * @param Grav $grav
  95. */
  96. public function __construct(Grav $grav)
  97. {
  98. $this->init($grav);
  99. }
  100. /**
  101. * Initialization that sets a base key and the driver based on configuration settings
  102. *
  103. * @param Grav $grav
  104. * @return void
  105. */
  106. public function init(Grav $grav)
  107. {
  108. /** @var Config $config */
  109. $this->config = $grav['config'];
  110. $this->now = time();
  111. if (null === $this->enabled) {
  112. $this->enabled = (bool)$this->config->get('system.cache.enabled');
  113. }
  114. /** @var Uri $uri */
  115. $uri = $grav['uri'];
  116. $prefix = $this->config->get('system.cache.prefix');
  117. $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
  118. // Cache key allows us to invalidate all cache on configuration changes.
  119. $this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
  120. $this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
  121. $this->driver_setting = $this->config->get('system.cache.driver');
  122. $this->driver = $this->getCacheDriver();
  123. $this->driver->setNamespace($this->key);
  124. /** @var EventDispatcher $dispatcher */
  125. $dispatcher = Grav::instance()['events'];
  126. $dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
  127. }
  128. /**
  129. * @return CacheInterface
  130. */
  131. public function getSimpleCache()
  132. {
  133. if (null === $this->simpleCache) {
  134. $cache = new \Grav\Framework\Cache\Adapter\DoctrineCache($this->driver, '', $this->getLifetime());
  135. // Disable cache key validation.
  136. $cache->setValidation(false);
  137. $this->simpleCache = $cache;
  138. }
  139. return $this->simpleCache;
  140. }
  141. /**
  142. * Deletes the old out of date file-based caches
  143. *
  144. * @return int
  145. */
  146. public function purgeOldCache()
  147. {
  148. $cache_dir = dirname($this->cache_dir);
  149. $current = basename($this->cache_dir);
  150. $count = 0;
  151. foreach (new DirectoryIterator($cache_dir) as $file) {
  152. $dir = $file->getBasename();
  153. if ($dir === $current || $file->isDot() || $file->isFile()) {
  154. continue;
  155. }
  156. Folder::delete($file->getPathname());
  157. $count++;
  158. }
  159. return $count;
  160. }
  161. /**
  162. * Public accessor to set the enabled state of the cache
  163. *
  164. * @param bool|int $enabled
  165. * @return void
  166. */
  167. public function setEnabled($enabled)
  168. {
  169. $this->enabled = (bool)$enabled;
  170. }
  171. /**
  172. * Returns the current enabled state
  173. *
  174. * @return bool
  175. */
  176. public function getEnabled()
  177. {
  178. return $this->enabled;
  179. }
  180. /**
  181. * Get cache state
  182. *
  183. * @return string
  184. */
  185. public function getCacheStatus()
  186. {
  187. return 'Cache: [' . ($this->enabled ? 'true' : 'false') . '] Setting: [' . $this->driver_setting . '] Driver: [' . $this->driver_name . ']';
  188. }
  189. /**
  190. * Automatically picks the cache mechanism to use. If you pick one manually it will use that
  191. * If there is no config option for $driver in the config, or it's set to 'auto', it will
  192. * pick the best option based on which cache extensions are installed.
  193. *
  194. * @return DoctrineCache\CacheProvider The cache driver to use
  195. */
  196. public function getCacheDriver()
  197. {
  198. $setting = $this->driver_setting;
  199. $driver_name = 'file';
  200. // CLI compatibility requires a non-volatile cache driver
  201. if ($this->config->get('system.cache.cli_compatibility') && (
  202. $setting === 'auto' || $this->isVolatileDriver($setting))) {
  203. $setting = $driver_name;
  204. }
  205. if (!$setting || $setting === 'auto') {
  206. if (extension_loaded('apcu')) {
  207. $driver_name = 'apcu';
  208. } elseif (extension_loaded('wincache')) {
  209. $driver_name = 'wincache';
  210. }
  211. } else {
  212. $driver_name = $setting;
  213. }
  214. $this->driver_name = $driver_name;
  215. switch ($driver_name) {
  216. case 'apc':
  217. case 'apcu':
  218. $driver = new DoctrineCache\ApcuCache();
  219. break;
  220. case 'wincache':
  221. $driver = new DoctrineCache\WinCacheCache();
  222. break;
  223. case 'memcache':
  224. if (extension_loaded('memcache')) {
  225. $memcache = new \Memcache();
  226. $memcache->connect(
  227. $this->config->get('system.cache.memcache.server', 'localhost'),
  228. $this->config->get('system.cache.memcache.port', 11211)
  229. );
  230. $driver = new DoctrineCache\MemcacheCache();
  231. $driver->setMemcache($memcache);
  232. } else {
  233. throw new LogicException('Memcache PHP extension has not been installed');
  234. }
  235. break;
  236. case 'memcached':
  237. if (extension_loaded('memcached')) {
  238. $memcached = new \Memcached();
  239. $memcached->addServer(
  240. $this->config->get('system.cache.memcached.server', 'localhost'),
  241. $this->config->get('system.cache.memcached.port', 11211)
  242. );
  243. $driver = new DoctrineCache\MemcachedCache();
  244. $driver->setMemcached($memcached);
  245. } else {
  246. throw new LogicException('Memcached PHP extension has not been installed');
  247. }
  248. break;
  249. case 'redis':
  250. if (extension_loaded('redis')) {
  251. $redis = new \Redis();
  252. $socket = $this->config->get('system.cache.redis.socket', false);
  253. $password = $this->config->get('system.cache.redis.password', false);
  254. $databaseId = $this->config->get('system.cache.redis.database', 0);
  255. if ($socket) {
  256. $redis->connect($socket);
  257. } else {
  258. $redis->connect(
  259. $this->config->get('system.cache.redis.server', 'localhost'),
  260. $this->config->get('system.cache.redis.port', 6379)
  261. );
  262. }
  263. // Authenticate with password if set
  264. if ($password && !$redis->auth($password)) {
  265. throw new \RedisException('Redis authentication failed');
  266. }
  267. // Select alternate ( !=0 ) database ID if set
  268. if ($databaseId && !$redis->select($databaseId)) {
  269. throw new \RedisException('Could not select alternate Redis database ID');
  270. }
  271. $driver = new DoctrineCache\RedisCache();
  272. $driver->setRedis($redis);
  273. } else {
  274. throw new LogicException('Redis PHP extension has not been installed');
  275. }
  276. break;
  277. default:
  278. $driver = new DoctrineCache\FilesystemCache($this->cache_dir);
  279. break;
  280. }
  281. return $driver;
  282. }
  283. /**
  284. * Gets a cached entry if it exists based on an id. If it does not exist, it returns false
  285. *
  286. * @param string $id the id of the cached entry
  287. * @return mixed|bool returns the cached entry, can be any type, or false if doesn't exist
  288. */
  289. public function fetch($id)
  290. {
  291. if ($this->enabled) {
  292. return $this->driver->fetch($id);
  293. }
  294. return false;
  295. }
  296. /**
  297. * Stores a new cached entry.
  298. *
  299. * @param string $id the id of the cached entry
  300. * @param array|object|int $data the data for the cached entry to store
  301. * @param int|null $lifetime the lifetime to store the entry in seconds
  302. */
  303. public function save($id, $data, $lifetime = null)
  304. {
  305. if ($this->enabled) {
  306. if ($lifetime === null) {
  307. $lifetime = $this->getLifetime();
  308. }
  309. $this->driver->save($id, $data, $lifetime);
  310. }
  311. }
  312. /**
  313. * Deletes an item in the cache based on the id
  314. *
  315. * @param string $id the id of the cached data entry
  316. * @return bool true if the item was deleted successfully
  317. */
  318. public function delete($id)
  319. {
  320. if ($this->enabled) {
  321. return $this->driver->delete($id);
  322. }
  323. return false;
  324. }
  325. /**
  326. * Deletes all cache
  327. *
  328. * @return bool
  329. */
  330. public function deleteAll()
  331. {
  332. if ($this->enabled) {
  333. return $this->driver->deleteAll();
  334. }
  335. return false;
  336. }
  337. /**
  338. * Returns a boolean state of whether or not the item exists in the cache based on id key
  339. *
  340. * @param string $id the id of the cached data entry
  341. * @return bool true if the cached items exists
  342. */
  343. public function contains($id)
  344. {
  345. if ($this->enabled) {
  346. return $this->driver->contains(($id));
  347. }
  348. return false;
  349. }
  350. /**
  351. * Getter method to get the cache key
  352. *
  353. * @return string
  354. */
  355. public function getKey()
  356. {
  357. return $this->key;
  358. }
  359. /**
  360. * Setter method to set key (Advanced)
  361. *
  362. * @param string $key
  363. * @return void
  364. */
  365. public function setKey($key)
  366. {
  367. $this->key = $key;
  368. $this->driver->setNamespace($this->key);
  369. }
  370. /**
  371. * Helper method to clear all Grav caches
  372. *
  373. * @param string $remove standard|all|assets-only|images-only|cache-only
  374. * @return array
  375. */
  376. public static function clearCache($remove = 'standard')
  377. {
  378. $locator = Grav::instance()['locator'];
  379. $output = [];
  380. $user_config = USER_DIR . 'config/system.yaml';
  381. switch ($remove) {
  382. case 'all':
  383. $remove_paths = self::$all_remove;
  384. break;
  385. case 'assets-only':
  386. $remove_paths = self::$assets_remove;
  387. break;
  388. case 'images-only':
  389. $remove_paths = self::$images_remove;
  390. break;
  391. case 'cache-only':
  392. $remove_paths = self::$cache_remove;
  393. break;
  394. case 'tmp-only':
  395. $remove_paths = self::$tmp_remove;
  396. break;
  397. case 'invalidate':
  398. $remove_paths = [];
  399. break;
  400. default:
  401. if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) {
  402. $remove_paths = self::$standard_remove;
  403. } else {
  404. $remove_paths = self::$standard_remove_no_images;
  405. }
  406. }
  407. // Delete entries in the doctrine cache if required
  408. if (in_array($remove, ['all', 'standard'])) {
  409. $cache = Grav::instance()['cache'];
  410. $cache->driver->deleteAll();
  411. }
  412. // Clearing cache event to add paths to clear
  413. Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
  414. foreach ($remove_paths as $stream) {
  415. // Convert stream to a real path
  416. try {
  417. $path = $locator->findResource($stream, true, true);
  418. if ($path === false) {
  419. continue;
  420. }
  421. $anything = false;
  422. $files = glob($path . '/*');
  423. if (is_array($files)) {
  424. foreach ($files as $file) {
  425. if (is_link($file)) {
  426. $output[] = '<yellow>Skipping symlink: </yellow>' . $file;
  427. } elseif (is_file($file)) {
  428. if (@unlink($file)) {
  429. $anything = true;
  430. }
  431. } elseif (is_dir($file)) {
  432. if (Folder::delete($file, false)) {
  433. $anything = true;
  434. }
  435. }
  436. }
  437. }
  438. if ($anything) {
  439. $output[] = '<red>Cleared: </red>' . $path . '/*';
  440. }
  441. } catch (Exception $e) {
  442. // stream not found or another error while deleting files.
  443. $output[] = '<red>ERROR: </red>' . $e->getMessage();
  444. }
  445. }
  446. $output[] = '';
  447. if (($remove === 'all' || $remove === 'standard') && file_exists($user_config)) {
  448. touch($user_config);
  449. $output[] = '<red>Touched: </red>' . $user_config;
  450. $output[] = '';
  451. }
  452. // Clear stat cache
  453. @clearstatcache();
  454. // Clear opcache
  455. if (function_exists('opcache_reset')) {
  456. @opcache_reset();
  457. }
  458. Grav::instance()->fireEvent('onAfterCacheClear', new Event(['remove' => $remove, 'output' => &$output]));
  459. return $output;
  460. }
  461. /**
  462. * @return void
  463. */
  464. public static function invalidateCache()
  465. {
  466. $user_config = USER_DIR . 'config/system.yaml';
  467. if (file_exists($user_config)) {
  468. touch($user_config);
  469. }
  470. // Clear stat cache
  471. @clearstatcache();
  472. // Clear opcache
  473. if (function_exists('opcache_reset')) {
  474. @opcache_reset();
  475. }
  476. }
  477. /**
  478. * Set the cache lifetime programmatically
  479. *
  480. * @param int $future timestamp
  481. * @return void
  482. */
  483. public function setLifetime($future)
  484. {
  485. if (!$future) {
  486. return;
  487. }
  488. $interval = (int)($future - $this->now);
  489. if ($interval > 0 && $interval < $this->getLifetime()) {
  490. $this->lifetime = $interval;
  491. }
  492. }
  493. /**
  494. * Retrieve the cache lifetime (in seconds)
  495. *
  496. * @return int
  497. */
  498. public function getLifetime()
  499. {
  500. if ($this->lifetime === null) {
  501. $this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default
  502. }
  503. return $this->lifetime;
  504. }
  505. /**
  506. * Returns the current driver name
  507. *
  508. * @return string
  509. */
  510. public function getDriverName()
  511. {
  512. return $this->driver_name;
  513. }
  514. /**
  515. * Returns the current driver setting
  516. *
  517. * @return string
  518. */
  519. public function getDriverSetting()
  520. {
  521. return $this->driver_setting;
  522. }
  523. /**
  524. * is this driver a volatile driver in that it resides in PHP process memory
  525. *
  526. * @param string $setting
  527. * @return bool
  528. */
  529. public function isVolatileDriver($setting)
  530. {
  531. if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
  532. return true;
  533. }
  534. return false;
  535. }
  536. /**
  537. * Static function to call as a scheduled Job to purge old Doctrine files
  538. *
  539. * @param bool $echo
  540. *
  541. * @return string|void
  542. */
  543. public static function purgeJob($echo = false)
  544. {
  545. /** @var Cache $cache */
  546. $cache = Grav::instance()['cache'];
  547. $deleted_folders = $cache->purgeOldCache();
  548. $msg = 'Purged ' . $deleted_folders . ' old cache folders...';
  549. if ($echo) {
  550. echo $msg;
  551. } else {
  552. return $msg;
  553. }
  554. }
  555. /**
  556. * Static function to call as a scheduled Job to clear Grav cache
  557. *
  558. * @param string $type
  559. * @return void
  560. */
  561. public static function clearJob($type)
  562. {
  563. $result = static::clearCache($type);
  564. static::invalidateCache();
  565. echo strip_tags(implode("\n", $result));
  566. }
  567. /**
  568. * @param Event $event
  569. * @return void
  570. */
  571. public function onSchedulerInitialized(Event $event)
  572. {
  573. /** @var Scheduler $scheduler */
  574. $scheduler = $event['scheduler'];
  575. $config = Grav::instance()['config'];
  576. // File Cache Purge
  577. $at = $config->get('system.cache.purge_at');
  578. $name = 'cache-purge';
  579. $logs = 'logs/' . $name . '.out';
  580. $job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [true], $name);
  581. $job->at($at);
  582. $job->output($logs);
  583. $job->backlink('/config/system#caching');
  584. // Cache Clear
  585. $at = $config->get('system.cache.clear_at');
  586. $clear_type = $config->get('system.cache.clear_job_type');
  587. $name = 'cache-clear';
  588. $logs = 'logs/' . $name . '.out';
  589. $job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name);
  590. $job->at($at);
  591. $job->output($logs);
  592. $job->backlink('/config/system#caching');
  593. }
  594. }