Cache.php 18 KB

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