Source of file Cacheable.php
Size: 22,922 Bytes - Last Modified: 2021-12-24T06:44:18+00:00
/var/www/docs.ssmods.com/process/src/code/extensions/Cacheable.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681 | <?php /** * * Gives {@link SiteTree} objects caching abilities. * * @author Deviate Ltd 2014-2015 http://www.deviate.net.nz * @package silverstripe-cachable * @todo Remove capitalised template-methods */ class Cacheable extends SiteTreeExtension { /** * * @var mixed */ public static $_cached_navigation; /** * * Initialises a pre-built cache (via {@link CacheableNavigation_Rebuild}) * used by front-end calling logic e.g. via $CachedData blocks in .ss templates * unless build_on_reload is set to false in YML config. * * Called using SilverStripe's extend() method in {@link ContentController}. * * @param Controller $controller * @return void * @see {@link CacheableNavigation_Rebuild}. * @see {@link CacheableNavigation_Clean}. * @todo add queuedjob chunking ala BuildTask to this csche-rebuild logic. * At the moment we attempt to skip it if build_cache_onload is set to false * in YML Config */ public function contentControllerInit($controller) { // Skip if flushing or the project instructs us to do so $skip = (self::is_flush($controller) || !self::build_cache_onload()); $service = new CacheableNavigationService(); $currentStage = Versioned::current_stage(); $stage_mode_mapping = array( "Stage" => "Stage", "Live" => "Live", ); $service->set_mode($stage_mode_mapping[$currentStage]); $siteConfig = SiteConfig::current_site_config(); if (!$siteConfig->exists()) { $siteConfig = $this->owner->getSiteConfig(); } $service->set_config($siteConfig); if ($_cached_navigation = $service->getCacheableFrontEnd()->load($service->getIdentifier())) { if (!$skip && !$_cached_navigation->get_completed()) { $service->refreshCachedConfig(); if (class_exists('Subsite')) { $pages = DataObject::get("Page", "\"SubsiteID\" = '".$siteConfig->SubsiteID."'"); } else { $pages = DataObject::get("Page"); } if ($pages->exists()) { foreach ($pages as $page) { $service->set_model($page); $service->refreshCachedPage(true); } } $service->completeBuild(); $_cached_navigation = $service->getCacheableFrontEnd()->load($service->getIdentifier()); } Config::inst()->update('Cacheable', '_cached_navigation', $_cached_navigation); } } public function onAfterWrite() { $this->refreshPageCache(array( 'Stage' => 'Stage', ), false); } public function onAfterPublish(&$original) { $this->refreshPageCache(array( 'Live' => 'Live', ), false); } public function onAfterUnpublish() { $this->removePageCache(array( 'Live' => 'Live', ), false); } public function onAfterDelete() { $this->removePageCache(array( 'Stage' => 'Stage', 'Live' => 'Live', ), false); $this->refreshPageCache(array( 'Stage' => 'Stage', 'Live' => 'Live', ), false); } /** * * @param array $modes * @param boolean $forceRemoval Whether to unset() children in {@link CacheableSiteTree::removeChild()}. * @return void */ public function refreshPageCache($modes, $forceRemoval = false) { // Increase memory to max-allowable CacheableConfig::configure_memory_limit(); //get the unlocked cached Navigation first $siteConfig = $this->owner->getSiteConfig(); if (!$siteConfig->exists()) { $siteConfig = SiteConfig::current_site_config(); } $currentStage = Versioned::current_stage(); foreach ($modes as $stage => $mode) { Versioned::reading_stage($stage); Versioned::set_default_reading_mode('Stage.'.$stage); $service = new CacheableNavigationService($mode, $siteConfig); $cache_frontend = $service->getCacheableFrontEnd(); $id = $service->getIdentifier(); $cached = $cache_frontend->load($id); if ($cached) { $cached_site_config = $cached->get_site_config(); if (!$cached_site_config) { $service->refreshCachedConfig(); } $versioned = Versioned::get_one_by_stage(get_class($this->owner), $stage, "\"SiteTree\".\"ID\" = '".$this->owner->ID."'"); if ($versioned && $versioned->exists()) { $service->set_model($versioned); $service->refreshCachedPage($forceRemoval); } } } Versioned::reading_stage($currentStage); Versioned::set_default_reading_mode('Stage.'.$currentStage); } /** * * @param array $modes * @param boolean $forceRemoval Whether to unset() children in {@link CacheableSiteTree::removeChild()}. * @return void */ public function removePageCache($modes, $forceRemoval = true) { // Increase memory to max-allowable CacheableConfig::configure_memory_limit(); $siteConfig = $this->owner->getSiteConfig(); if (!$siteConfig->exists()) { $siteConfig = SiteConfig::current_site_config(); } $currentStage = Versioned::current_stage(); foreach ($modes as $stage => $mode) { Versioned::reading_stage($stage); Versioned::set_default_reading_mode('Stage.'.$stage); $service = new CacheableNavigationService($mode, $siteConfig, $this->owner); $cache_frontend = $service->getCacheableFrontEnd(); $id = $service->getIdentifier(); $cached = $cache_frontend->load($id); if ($cached) { $cached_site_config = $cached->get_site_config(); if (!$cached_site_config) { $service->refreshCachedConfig(); } $service->removeCachedPage($forceRemoval); } } Versioned::reading_stage($currentStage); Versioned::set_default_reading_mode('Stage.'.$currentStage); } /** * * @return mixed ContentController | array */ public function CachedNavigation() { if ($this->owner->exists()) { if ($cachedNavigiation = Config::inst()->get('Cacheable', '_cached_navigation')) { if ($cachedNavigiation->isUnlocked() && $cachedNavigiation->get_completed()) { return $cachedNavigiation; } } } return new ContentController($this->owner); } /** * * Usually used in template logic inside <% with %> blocks. * * @see README.md * @return mixed ContentController | array * @todo What to do with controller URLs other than returning the homepage's cache? */ public function CachedData() { if ($cachedNavigiation = Config::inst()->get('Cacheable', '_cached_navigation')) { if ($cachedNavigiation->isUnlocked() && $cachedNavigiation->get_completed()) { $site_map = $cachedNavigiation->get_site_map(); if (!empty($site_map[$this->owner->ID])) { return $site_map[$this->owner->ID]; } /* * Prevent errors and go-slows for controller URLs e.g. /admin * and return the homepage's cache as a 'sensible' default */ // check if isset($site_map)[1]; since there is some page created on flying, login page, search page, etc, etc if (isset($site_map[1])) { return $site_map[1]; } } } return new ContentController($this->owner); } /** * * Detect if a flush operation is happening. * * @param Controller $controller * @return boolean * @todo add tests */ public static function is_flush(Controller $controller) { $getVars = $controller->getRequest()->getVars(); return (stristr(implode(',', array_keys($getVars)), 'flush') !== false); } /** * * Build an array of object-cache files from the filesystem. * * @return array */ public static function get_cache_files() { $files = array(); foreach (scandir(CACHEABLE_STORE_DIR) as $file) { // Ignore hidden files if (!preg_match("#^(\.|web\.)#", $file)) { $name = CACHEABLE_STORE_DIR . DIRECTORY_SEPARATOR . $file; if (file_exists($name)) { $size = filesize($name); $date = date('Y-m-d H:i:s', filemtime($name)); $files[$name] = ArrayData::create(array( 'Line' => $size . "\t" . $date . "\t" . $file, 'Size' => $size, 'Date' => $date )); } } } return $files; } /** * * Current module default is to build the cache if it's not present via * a browser request after the "first user pays" pattern. This may not be * desirable on sites with 1000s of page objects. * * @return boolean * @todo Add tests */ public static function build_cache_onload() { return (bool) Config::inst()->get('CacheableConfig', 'build_cache_onload'); } // TODO: Remove public $start_time; public function StartTime() { $this->start_time = time(); return '<br />starting at '.$this->start_time."<br />"; } public $end_time; public function EndTime() { $this->end_time = time(); return '<br />ending at '.$this->end_time."<br />"; } public function TimeConsumed() { return '<br />time consumed: '.((int)$this->end_time-(int)$this->start_time)."<br />"; } } /** * * @author Deviate Ltd 2014-2015 http://www.deviate.net.nz * @package silverstripe-cachable * @todo Add unit tests to ensure exceptions are thrown in correct circumstances * @todo Move this into own class file */ class CacheableConfig { /** * * @var string */ private static $default_mode = 'file'; /** * * Get us the current caching mode. Useful for debugging * * @var string */ protected static $current_mode = ''; /** * * Used for the new memory_limit value for display in arbitrary calling logic. * * @var int */ public static $ini_modified_memory_limit = 0; /** * * On smaller setups, CWP being one; allow the module as much RAM as it can offer. * See the URL below for why we cannot go above this in CWP, read: Suhosin. * * @see https://www.cwp.govt.nz/guides/technical-faq/php-configuration/. * @todo Make generic: Check for Suhosin memory limit. Increase memory if current allocation < Suhosin allows * @return void */ public static function configure_memory_limit() { // In testing, with sites in excess of 1000 pages, we've not seen anything greater // than 170Mb per queued chunk-set $newLimit = 256; // upper limit of CWP "small" instances becuase of Suhosin if (defined('CWP_ENVIRONMENT') && intval(ini_get('memory_limit')) < $newLimit) { ini_set('memory_limit', $newLimit . 'M'); self::$ini_modified_memory_limit = $newLimit; } } /** * * @return boolean True if "Memcached" extension is loaded */ public static function configure_memcached() { if (extension_loaded('memcached')) { $defaultOpts = array( 'servers' => array( 'host' => '127.0.0.1', 'port' => 11211, 'weight' => 1 ), 'client' => array( Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT, Memcached::OPT_HASH => Memcached::HASH_MD5, Memcached::OPT_LIBKETAMA_COMPATIBLE => true, ) ); // Use project-specific overridden opts, or the defaults $projectOpts = Config::inst()->get('CacheableConfig', 'opts'); $serverOpts = ($projectOpts && !empty($projectOpts['memcached'])) ? $projectOpts['memcached']['servers'] : $defaultOpts['servers']; $clientOpts = ($projectOpts && !empty($projectOpts['memcached'])) ? $projectOpts['memcached']['client'] : $defaultOpts['client']; // Libmemcached is enabled. SS_Cache::add_backend(CACHEABLE_STORE_NAME, 'Libmemcached', array( 'servers' => array($serverOpts), 'client' => $clientOpts ) ); self::$current_mode = 'memcached'; return true; } return false; } /** * * @return boolean True if "Memcache" extension is loaded */ public static function configure_memcache() { $defaultOpts = array( 'servers' => array( 'host' => 'localhost', 'port' => 11211, 'persistent' => true, 'weight' => 1, 'timeout' => 5, 'retry_interval' => 15, 'status' => true, 'failure_callback' => '' ) ); if (class_exists('Memcache')) { // Use project-specific overridden opts, or the defaults $projectOpts = Config::inst()->get('CacheableConfig', 'opts'); $serverOpts = ($projectOpts && !empty($projectOpts['memcache']['servers'])) ? $projectOpts['memcache']['servers'] : $defaultOpts['servers']; // Memcached is enabled. SS_Cache::add_backend( CACHEABLE_STORE_NAME, 'Memcache', array( 'servers' => $serverOpts['servers'] ) ); self::$current_mode = 'memcache'; return true; } return false; } /** * * @return boolean True if the filesystem is available to be used for caching. */ public static function configure_file() { $cacheable_store_dir = self::is_running_test() ? CACHEABLE_STORE_DIR_TEST : CACHEABLE_STORE_DIR; if (!is_dir($cacheable_store_dir)) { mkdir($cacheable_store_dir, 0775); } $storeIsOk = file_exists($cacheable_store_dir); if (!$storeIsOk) { return false; } // Write server config files to cache-dir if it should exist relative to "assets" self::protect_cache_dir(); // Update SilverStripe so it leaves cache-dirs under "assets" alone when attempting to sync assets self::prevent_cache_dir_sync(); SS_Cache::add_backend(CACHEABLE_STORE_NAME, 'File', array( 'cache_dir' => $cacheable_store_dir, 'read_control' => false, // If true, fails to load cache when Queueing enabled. 'file_name_prefix' => 'cacheable_cache', 'file_locking' => true )); self::$current_mode = 'file'; return true; } /** * * @return boolean True if APCu is installed as an extension and is enabled in php.ini */ public static function configure_apc() { $isApcEnabled = (extension_loaded('apc') && ini_get('apc.enabled') == 1); if (!$isApcEnabled) { return false; } SS_Cache::add_backend(CACHEABLE_STORE_NAME, 'Apc'); self::$current_mode = 'apc'; return true; } /** * * @throws CacheableException * @return void */ public static function configure() { // Project-specific YML config trumps anything in module's _config.php if (!$mode = Config::inst()->get('CacheableConfig', 'cache_mode')) { $mode = self::$default_mode; } $confMethodName = 'configure_' . $mode; if (!method_exists(get_class(), $confMethodName)) { throw new CacheableException('The configured cache mode: "$mode" doesn\'t exist.'); } // Default to "File" backend if one of the modes isn't playing ball if (!self::$confMethodName()) { if (!self::configure_file()) { throw new CacheableException('Unable to select a cache backend. Giving up.'); } } } /** * * Returns the current cache mode. * * @return string */ public static function current_cache_mode() { return self::$current_mode; } /** * SapphireTest::is_running_test() returns false at this point, and there is * no Controller available either so we need an alternate way of detecting if * tests are running. * * Ideally we'd be using mocking, so this hack wouldn't be necessary. * * @return boolean */ public static function is_running_test() { if (isset($_REQUEST['url'])) { return stristr($_REQUEST['url'], 'dev/tests') !== false; } return false; } /** * * Deal with userland alternate cache location for the "File" backend. This should * always be relative to the assets dir for portability. * * The default if no such setting is present, is to place the cache directory * beneath SilverStripe's tmp dir. * * @return string */ public static function cache_dir_path() { $altCacheDir = Config::inst()->get('CacheableConfig', 'alt_cache_dir'); $charMask = " \t\n\r\0\x0B/"; if ($altCacheDir) { $altDir = trim($altCacheDir, $charMask); // If alt_cache_dir matches "cacheable", just use that if ($altDir === CACHEABLE_STORE_DIR_NAME) { $cacheDir = '_' . CACHEABLE_STORE_DIR_NAME; } else { $cacheDir = '_' . $altDir . DIRECTORY_SEPARATOR . CACHEABLE_STORE_DIR_NAME; } $cacheDir = ASSETS_PATH . DIRECTORY_SEPARATOR . $cacheDir; } else { $cacheDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . CACHEABLE_STORE_DIR_NAME; } return $cacheDir; } /** * * Return the URI relative-to, and including the "assets" folder. * * @param boolean $withAssets * @return boolean|string */ public static function cache_dir_location($withAssets = true) { $cacheDirPath = self::cache_dir_path(); $isCacheDirRelative = stristr($cacheDirPath, ASSETS_DIR) !== false; if ($isCacheDirRelative) { $uriTruncated = substr($cacheDirPath, 0, strpos($cacheDirPath, ASSETS_DIR)); $result = str_replace($uriTruncated, '', $cacheDirPath); if ($withAssets) { return '/' . $result; } return str_replace(ASSETS_DIR, '', $result); } return false; } /** * * Simply return the name of the bottom-level dir in which cache files will be stored. * Takes into account userland config viz `alt_cache_dir`. * * Examples: * * - alt_cache_dir: 'foo/bar' --> "bar" * - alt_cache_dir: 'cacheable' --> "cacheable" * * @param string $cacheDir * @return string */ public static function cache_dir_name($cacheDir) { return pathinfo($cacheDir, PATHINFO_FILENAME); } /** * * Generate files appropriate to the host webserver for protecting access to the * cache dir TTW. Only in use when using "File" backend via {@link self::configure_file()} * and the userland `alt_cache_dir` config is present. * * @return void */ private static function protect_cache_dir() { $altCacheDir = Config::inst()->get('CacheableConfig', 'alt_cache_dir'); $isHttpdConf = file_exists(self::cache_dir_path() . DIRECTORY_SEPARATOR . '.htaccess'); if ($altCacheDir && !$isHttpdConf) { // Source SS template files for copying into final config $resourceDir = CACHEABLE_MODULE_DIR . DIRECTORY_SEPARATOR . 'templates/resources'; $httpdConf = file_get_contents($resourceDir . DIRECTORY_SEPARATOR . 'htaccess.ss'); $iisConf = file_get_contents($resourceDir . DIRECTORY_SEPARATOR . 'webconfig.ss'); file_put_contents(self::cache_dir_path() . DIRECTORY_SEPARATOR . '.htaccess', $httpdConf); file_put_contents(self::cache_dir_path() . DIRECTORY_SEPARATOR . 'web.config', $iisConf); } } /** * * Update SilverStripe's understanding of the full range of "unsyncable" asset * folder sub-dirs. Only called in the context of the "File" backend and if userland * config setting exists for `alt_cache_dir`. * * Examples: * * - alt_cache_dir: '/foo/bar' --> "^/_foo$/i" is added to Filesystem::sync_blacklisted_patterns. * - alt_cache_dir: '/cacheable' --> "^/_cacheable$/i" is added to Filesystem::sync_blacklisted_patterns. * * @see Unit tests for {@link self::cache_dir_path()} for more context. * @return void */ public static function prevent_cache_dir_sync() { $altCacheDir = Config::inst()->get('CacheableConfig', 'alt_cache_dir'); if ($altCacheDir) { $cachePathLocation = self::cache_dir_location(false); $cachePathParts = explode('/', ltrim($cachePathLocation, '/')); $pattern = "/^" . $cachePathParts[0] . "$/i"; Config::inst()->update('Filesystem', 'sync_blacklisted_patterns', array($pattern)); } } } /** * * Custom exceptions allow module-specific exceptions to be easily tracked * when such tracking/alerting systems are utilised. * * @author Deviate Ltd 2015 http://www.deviate.net.nz * @package silverstripe-cachable */ class CacheableException extends Exception { } |