ÿØÿà 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-Ї Ç PK\$vhhlang/en/cachestore_session.phpnu[. /** * 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.'; PK\f2/~G~Glib.phpnu[. /** * 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; } } PK\` version.phpnu[. /** * 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.PK\ta>''classes/privacy/provider.phpnu[. /** * 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) { } } PK\,a4hCCtests/store_test.phpnu[. 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'))); } } PK"\Dx(( memcached.phpnu[. namespace core\session; /** * Memcached based session handler. * * @package core * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class memcached extends handler { /** @var string $savepath save_path string */ protected $savepath; /** @var array $servers list of servers parsed from save_path */ protected $servers; /** @var string $prefix session key prefix */ protected $prefix; /** @var int $acquiretimeout how long to wait for session lock */ protected $acquiretimeout = 120; /** * @var int $lockexpire how long to wait before expiring the lock so that other requests * may continue execution, ignored if PECL memcached is below version 2.2.0. */ protected $lockexpire = 7200; /** * @var integer $lockretrysleep Used for memcached 3.x (PHP7), the amount of time to * sleep between attempts to acquire the session lock. Mimics the deprecated config * memcached.sess_lock_wait. */ protected $lockretrysleep = 150; /** * Create new instance of handler. */ public function __construct() { global $CFG; if (empty($CFG->session_memcached_save_path)) { $this->savepath = ''; } else { $this->savepath = $CFG->session_memcached_save_path; } if (empty($this->savepath)) { $this->servers = array(); } else { $this->servers = self::connection_string_to_memcache_servers($this->savepath); } if (empty($CFG->session_memcached_prefix)) { $this->prefix = ini_get('memcached.sess_prefix'); } else { $this->prefix = $CFG->session_memcached_prefix; } if (!empty($CFG->session_memcached_acquire_lock_timeout)) { $this->acquiretimeout = (int)$CFG->session_memcached_acquire_lock_timeout; } if (!empty($CFG->session_memcached_lock_expire)) { $this->lockexpire = (int)$CFG->session_memcached_lock_expire; } if (!empty($CFG->session_memcached_lock_retry_sleep)) { $this->lockretrysleep = (int)$CFG->session_memcached_lock_retry_sleep; } } #[\Override] public function start() { ini_set('memcached.sess_locking', $this->requires_write_lock() ? '1' : '0'); // NOTE: memcached before 2.2.0 expires session locks automatically after max_execution_time, // this leads to major difference compared to other session drivers that timeout // and stop the second request execution instead. $default = ini_get('max_execution_time'); set_time_limit($this->acquiretimeout); $isnewsession = empty($_COOKIE[session_name()]); $starttimer = microtime(true); $result = parent::start(); // If session_start returned TRUE, but it took as long // as the timeout value, and the $_SESSION returned is // empty when should not have been (isnewsession false) // then assume it did timeout and is invalid. // Add 1 second to elapsed time to account for inexact // timings in php_memcached_session.c. // @TODO Remove this check when php-memcached is fixed // to return false after key lock acquisition timeout. if (!$isnewsession && $result && count($_SESSION) == 0 && (microtime(true) - $starttimer + 1) >= floatval($this->acquiretimeout)) { $result = false; } set_time_limit($default); return $result; } #[\Override] public function init() { if (!extension_loaded('memcached')) { throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension is not loaded'); } $version = phpversion('memcached'); if (!$version or version_compare($version, '2.0') < 0) { throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension version must be at least 2.0'); } if (empty($this->savepath)) { throw new exception('sessionhandlerproblem', 'error', '', null, '$CFG->session_memcached_save_path must be specified in config.php'); } ini_set('session.save_handler', 'memcached'); ini_set('session.save_path', $this->savepath); ini_set('memcached.sess_prefix', $this->prefix); ini_set('memcached.sess_lock_expire', $this->lockexpire); if (version_compare($version, '3.0.0-dev') >= 0) { // With memcached 3.x (PHP 7) we configure the max retries to make and the time to sleep between each retry. // There are two sleep config values, an initial and a max value. // After each attempt the memcached module adjusts the sleep value to be the lesser of the configured max // value, or 2X the previous value. // With default memcached.ini configs (5, 1s, 2s) the result is only 5 attempts to lock over 9 sec. // To mimic the behavior of the 2.2.x module so we get more attempts and much more frequently, config both // sleep values to the old default value of 150 msec (making it constant) and calculate number of retries // using the existing Moodle config $CFG->session_memcached_acquire_lock_timeout. // Doing this so admins configure session lock attempt timeout in familiar terms, and more straight-forward // to detect if lock attempt timeout has occurred in start(). // If _min and _max values are not equal, the actual lock acquire timeout will not be the expected // configured value in $CFG->session_memcached_acquire_lock_timeout; this will cause session data loss when // failure to acquire the lock is not detected. ini_set('memcached.sess_lock_wait_min', $this->lockretrysleep); ini_set('memcached.sess_lock_wait_max', $this->lockretrysleep); ini_set('memcached.sess_lock_retries', (int)(($this->acquiretimeout * 1000) / $this->lockretrysleep) + 1); } else { // With memcached 2.2.x we configure max time to attempt lock, and accept default value (in memcached.ini) // for sleep time between each attempt (usually 150 msec), then memcached calculates the max number of // retries to make. ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout); } } #[\Override] public function session_exists($sid) { if (!$this->servers) { return false; } // Go through the list of all servers because // we do not know where the session handler put the // data. foreach ($this->servers as $server) { list($host, $port) = $server; $memcached = new \Memcached(); $memcached->addServer($host, $port); $value = $memcached->get($this->prefix . $sid); $memcached->quit(); if ($value !== false) { return true; } } return false; } #[\Override] public function destroy_all(): bool { global $DB; if (!$this->servers) { return false; } // Go through the list of all servers because // we do not know where the session handler put the // data. $memcacheds = array(); foreach ($this->servers as $server) { list($host, $port) = $server; $memcached = new \Memcached(); $memcached->addServer($host, $port); $memcacheds[] = $memcached; } // Note: this can be significantly improved by fetching keys from memcached, // but we need to make sure we are not deleting somebody else's sessions. $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid'); foreach ($rs as $record) { foreach ($memcacheds as $memcached) { $memcached->delete($this->prefix . $record->sid); } } $rs->close(); foreach ($memcacheds as $memcached) { $memcached->quit(); } return parent::destroy_all(); } #[\Override] public function destroy(string $id): bool { if (!$this->servers) { return false; } // Go through the list of all servers because // we do not know where the session handler put the // data. foreach ($this->servers as $server) { list($host, $port) = $server; $memcached = new \Memcached(); $memcached->addServer($host, $port); $memcached->delete($this->prefix . $id); $memcached->quit(); } return parent::destroy($id); } /** * Convert a connection string to an array of servers. * * "abc:123, xyz:789" to * [ * ['abc', '123'], * ['xyz', '789'], * ] * * @param string $str save_path value containing memcached connection string * @return array[] */ protected static function connection_string_to_memcache_servers(string $str): array { $servers = []; $parts = explode(',', $str); foreach ($parts as $part) { $part = trim($part); $pos = strrpos($part, ':'); if ($pos !== false) { $host = substr($part, 0, $pos); $port = substr($part, ($pos + 1)); } else { $host = $part; $port = 11211; } $servers[] = [$host, $port]; } return $servers; } } PK"\زloginas_helper.phpnu[. namespace core\session; use core\context; use core\context\course as context_course; use core\context\system as context_system; use core\session\manager as sessionmanager; use stdClass; /** * Helper functions for the 'login as' feature. * * @package core * @author Jason den Dulk * @copyright 2025 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class loginas_helper { /** * Determine which context a user can login as another user for, given the requested target user and course. If the * person cannot login at all, then null will be returned. * * @param stdClass $currentuser The current user. Defaults to $USER. * @param stdClass $loginasuser The user to be logged in as. * @param stdClass|null $course The course currently being looked at. Applies to those who can only login within a * course context. If null, then only system context will be considered. * @return context|null Returns the context that the user is able to login as, or null if no context can be found. */ public static function get_context_user_can_login_as( stdClass $currentuser, stdClass $loginasuser, ?stdClass $course = null ): ?context { $systemcontext = context_system::instance(); if ( $loginasuser->deleted || // Can't login as a user that has been removed. $currentuser->id == $loginasuser->id || // Pointless for a user to login as himself. sessionmanager::is_loggedinas() // Already logged in as someone. ) { return null; } // Site admins can always login as someone else. if (is_siteadmin($currentuser)) { return $systemcontext; } // Non admins can never login as an admin. if (is_siteadmin($loginasuser)) { return null; } // Now, we accept anyone who can loginas at the site level, and they can do so at the system level. if (has_capability('moodle/user:loginas', $systemcontext, $currentuser)) { return $systemcontext; } if (!empty($course)) { $coursecontext = context_course::instance($course->id); if ( // Reject all that do not have loginas capability. !has_capability('moodle/user:loginas', $coursecontext, $currentuser) || // Reject if user is trying to login as someone with site level loginas powers. has_capability('moodle/user:loginas', $systemcontext, $loginasuser) || // Reject if other is not enrolled. !is_enrolled($coursecontext, $loginasuser->id) ) { return null; } // Check if the users are in the same group. if (groups_get_course_groupmode($course) == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext, $currentuser)) { $samegroup = false; if ($groups = groups_get_all_groups($course->id, $currentuser->id)) { foreach ($groups as $group) { if (groups_is_member($group->id, $loginasuser->id)) { $samegroup = true; break; } } } if (!$samegroup) { return null; } } // Passed all checks. return $coursecontext; } return null; } } PK"\`aZ external.phpnu[. /** * This class contains a list of webservice functions related to session. * * @package core * @copyright 2019 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\session; use core_external\external_api; use core_external\external_description; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; /** * This class contains a list of webservice functions related to session. * * @copyright 2019 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 2.9 */ class external extends external_api { /** * Returns description of touch_session() parameters. * * @return external_function_parameters */ public static function touch_session_parameters() { return new external_function_parameters([]); } /** * Extend the current session. * * @return array the mapping */ public static function touch_session() { \core\session\manager::touch_session(session_id()); return true; } /** * Returns description of touch_session() result value. * * @return external_description */ public static function touch_session_returns() { return new external_value(PARAM_BOOL, 'result'); } /** * Returns description of time_remaining() parameters. * * @return external_function_parameters */ public static function time_remaining_parameters() { return new external_function_parameters([]); } /** * Extend the current session. * * @return array the mapping */ public static function time_remaining() { return \core\session\manager::time_remaining(session_id()); } /** * Returns description of touch_session() result value. * * @return external_description */ public static function time_remaining_returns() { return new external_single_structure([ 'userid' => new external_value(PARAM_INT, 'The current user id.'), 'timeremaining' => new external_value(PARAM_INT, 'The number of seconds remaining in this session.'), ]); } } PK"\J database.phpnu[. namespace core\session; use SessionHandlerInterface; /** * Database based session handler. * * @package core * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class database extends handler implements SessionHandlerInterface { /** @var int $record session record */ protected $recordid = null; /** @var \moodle_database $database session database */ protected $database = null; /** @var bool $failed session read/init failed, do not write back to DB */ protected $failed = false; /** @var string $lasthash hash of the session data content */ protected $lasthash = null; /** @var int $acquiretimeout how long to wait for session lock */ protected $acquiretimeout = 120; /** * Create new instance of handler. */ public function __construct() { global $DB, $CFG; // Note: we store the reference here because we need to modify database in shutdown handler. $this->database = $DB; if (!empty($CFG->session_database_acquire_lock_timeout)) { $this->acquiretimeout = (int)$CFG->session_database_acquire_lock_timeout; } } #[\Override] public function init() { if (!$this->database->session_lock_supported()) { throw new exception('sessionhandlerproblem', 'error', '', null, 'Database does not support session locking'); } $result = session_set_save_handler($this); if (!$result) { throw new exception('dbsessionhandlerproblem', 'error'); } } #[\Override] public function session_exists($sid) { // It was already checked in the calling code that the record in sessions table exists. return true; } #[\Override] public function destroy(string $id): bool { if (!$session = $this->database->get_record('sessions', ['sid' => $id], 'id, sid')) { if ($id == session_id()) { $this->recordid = null; $this->lasthash = null; } return true; } if ($this->recordid && ($session->id == $this->recordid)) { try { $this->database->release_session_lock($this->recordid); } catch (\Exception $ex) { // Log and ignore any problems. mtrace('Failed to release session lock: '.$ex->getMessage()); } $this->recordid = null; $this->lasthash = null; } $this->database->delete_records('sessions', ['id' => $session->id]); return true; } /** * Open session handler. * * {@see http://php.net/manual/en/function.session-set-save-handler.php} * * @param string $path * @param string $name * @return bool success */ public function open(string $path, string $name): bool { // Note: we use the already open database. return true; } /** * Close session handler. * * {@see http://php.net/manual/en/function.session-set-save-handler.php} * * @return bool success */ public function close(): bool { if ($this->recordid) { try { $this->database->release_session_lock($this->recordid); } catch (\Exception $ex) { // Ignore any problems. } } $this->recordid = null; $this->lasthash = null; return true; } /** * Read session handler. * * {@see http://php.net/manual/en/function.session-set-save-handler.php} * * @param string $sid * @return string|false */ public function read(string $sid): string|false { try { if (!$record = $this->database->get_record('sessions', ['sid' => $sid])) { // Let's cheat and skip locking if this is the first access, // do not create the record here, let the manager do it after session init. $this->failed = false; $this->recordid = null; $this->lasthash = sha1(''); return ''; } if ($this->recordid and $this->recordid != $record->id) { error_log('Second session read with different record id detected, cannot read session'); $this->failed = true; $this->recordid = null; return ''; } if (!$this->recordid) { // Lock session if exists and not already locked. if ($this->requires_write_lock()) { $this->database->get_session_lock($record->id, $this->acquiretimeout); } $this->recordid = $record->id; } } catch (\dml_sessionwait_exception $ex) { // This is a fatal error, better inform users. // It should not happen very often - all pages that need long time to execute // should close session immediately after access control checks. error_log('Cannot obtain session lock for sid: '.$sid); $this->failed = true; throw $ex; } catch (\Exception $ex) { // Do not rethrow exceptions here, this should not happen. error_log('Unknown exception when starting database session : '.$sid.' - '.$ex->getMessage()); $this->failed = true; $this->recordid = null; return ''; } // Finally read the full session data because we know we have the lock now. if (!$record = $this->database->get_record('sessions', array('id'=>$record->id), 'id, sessdata')) { // Ignore - something else just deleted the session record. $this->failed = true; $this->recordid = null; return ''; } $this->failed = false; if (is_null($record->sessdata)) { $data = ''; $this->lasthash = sha1(''); } else { $data = base64_decode($record->sessdata); $this->lasthash = sha1($record->sessdata); } return $data; } /** * Write session handler. * * {@see http://php.net/manual/en/function.session-set-save-handler.php} * * NOTE: Do not write to output or throw any exceptions! * Hopefully the next page is going to display nice error or it recovers... * * @param string $id * @param string $data * @return bool success */ public function write(string $id, string $data): bool { if ($this->failed) { // Do not write anything back - we failed to start the session properly. return false; } // There might be some binary mess. $sessdata = base64_encode($data); $hash = sha1($sessdata); if ($hash === $this->lasthash) { return true; } try { if ($this->recordid) { $this->database->set_field('sessions', 'sessdata', $sessdata, ['id' => $this->recordid]); } else { // This happens in the first request when session record was just created in manager. $this->database->set_field('sessions', 'sessdata', $sessdata, ['sid' => $id]); } } catch (\Exception $ex) { // Do not rethrow exceptions here, this should not happen. // phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative error_log( "Unknown exception when writing database session data : {$id} - " . $ex->getMessage(), ); } return true; } } PK"\H! exception.phpnu[. /** * Session exception. * * @package core * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\session; defined('MOODLE_INTERNAL') || die(); /** * Session related exception class. * @package core */ class exception extends \moodle_exception { } PK"\MOf0 0 file.phpnu[. namespace core\session; /** * File based session handler. * * @package core * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class file extends handler { /** @var string session dir */ protected $sessiondir; /** * Create new instance of handler. */ public function __construct() { global $CFG; if (!empty($CFG->session_file_save_path)) { $this->sessiondir = $CFG->session_file_save_path; } else { $this->sessiondir = "$CFG->dataroot/sessions"; } } #[\Override] public function init() { if (preg_match('/^[0-9]+;/', $this->sessiondir)) { throw new exception('sessionhandlerproblem', 'error', '', null, 'Multilevel session directories are not supported'); } // Make sure session directory exists and is writable. make_writable_directory($this->sessiondir, false); if (!is_writable($this->sessiondir)) { throw new exception('sessionhandlerproblem', 'error', '', null, 'Session directory is not writable'); } // Need to disable debugging since disk_free_space() // will fail on very large partitions (see MDL-19222). // Moodle supports disable_functions = disk_free_space (MDL-43039). $freespace = function_exists('disk_free_space') ? disk_free_space($this->sessiondir) : false; if (!($freespace > 2048) && ($freespace !== false)) { throw new exception('sessiondiskfull', 'error'); } // NOTE: we cannot set any lock acquiring timeout here - bad luck. ini_set('session.save_handler', 'files'); ini_set('session.save_path', $this->sessiondir); } #[\Override] public function session_exists($sid) { $sid = clean_param($sid, PARAM_FILE); if (!$sid) { return false; } $sessionfile = "$this->sessiondir/sess_$sid"; return file_exists($sessionfile); } #[\Override] public function destroy_all(): bool { if (is_dir($this->sessiondir)) { foreach (glob("$this->sessiondir/sess_*") as $filename) { @unlink($filename); } } return parent::destroy_all(); } #[\Override] public function destroy(string $id): bool { $sid = clean_param($id, PARAM_FILE); if (!$sid) { return false; } $sessionfile = "$this->sessiondir/sess_$sid"; if (file_exists($sessionfile)) { @unlink($sessionfile); } return parent::destroy($id); } } PK"\Q manager.phpnu[. namespace core\session; use core\clock; use core\di; use html_writer; /** * Session manager, this is the public Moodle API for sessions. * * Following PHP functions MUST NOT be used directly: * - session_start() - not necessary, lib/setup.php starts session automatically, * use define('NO_MOODLE_COOKIE', true) if session not necessary. * - session_write_close() - use \core\session\manager::write_close() instead. * - session_destroy() - use require_logout() instead. * * @package core * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** @var int A hard cutoff of maximum stored history */ const MAXIMUM_STORED_SESSION_HISTORY = 50; /** @var int The recent session locks array is reset if there is a time gap more than this value in seconds */ const SESSION_RESET_GAP_THRESHOLD = 1; /** @var handler $handler active session handler instance */ protected static $handler; /** @var bool $sessionactive Is the session active? */ protected static $sessionactive = null; /** @var string $logintokenkey Key used to get and store request protection for login form. */ protected static $logintokenkey = 'core_auth_login'; /** @var array Stores the the SESSION before a request is performed, used to check incorrect read-only modes */ private static $priorsession = []; /** @var array Stores the the SESSION after write_close is called, used to check if it was mutated after the session is closed */ private static $sessionatclose = []; /** * @var bool Used to trigger the SESSION mutation warning without actually preventing SESSION mutation. * This variable is used to "copy" what the $requireslock parameter does in start_session(). * Once requireslock is set in start_session it's later accessible via $handler->requires_write_lock, * When using $CFG->enable_read_only_sessions_debug mode, this variable serves the same purpose without * actually setting the handler as requiring a write lock. */ private static $requireslockdebug; /** * If the current session is not writeable, abort it, and re-open it * requesting (and blocking) until a write lock is acquired. * If current session was already opened with an intentional write lock, * this call will not do anything. * NOTE: Even when using a session handler that does not support non-locking sessions, * if the original session was not opened with the explicit intention of being locked, * this will still restart your session so that code behaviour matches as closely * as practical across environments. * * @param bool $readonlysession Used by debugging logic to determine if whatever * triggered the restart (e.g., a webservice) declared * itself as read only. */ public static function restart_with_write_lock(bool $readonlysession) { global $CFG; if (!empty($CFG->enable_read_only_sessions) || !empty($CFG->enable_read_only_sessions_debug)) { self::$requireslockdebug = !$readonlysession; } if (self::$sessionactive && !self::$handler->requires_write_lock()) { @self::$handler->abort(); self::$sessionactive = false; self::start_session(true); } } /** * Start user session. * * Note: This is intended to be called only from lib/setup.php! */ public static function start() { global $CFG, $DB, $PERF; if (isset(self::$sessionactive)) { debugging('Session was already started!', DEBUG_DEVELOPER); return; } // Grab the time before session lock starts. $PERF->sessionlock['start'] = microtime(true); self::load_handler(); // Init the session handler only if everything initialised properly in lib/setup.php file // and the session is actually required. if (empty($DB) or empty($CFG->version) or !defined('NO_MOODLE_COOKIES') or NO_MOODLE_COOKIES or CLI_SCRIPT) { self::$sessionactive = false; self::init_empty_session(); return; } if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) { $requireslock = !READ_ONLY_SESSION; } else { $requireslock = true; // For backwards compatibility, we default to assuming that a lock is needed. } // By default make the two variables the same. This means that when they are // checked below in start_session and write_close there is no possibility for // the debug version to "accidentally" execute the debug mode. self::$requireslockdebug = $requireslock; if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions_debug)) { // Only change the debug variable if READ_ONLY_SESSION is declared, // as would happen with the real requireslock variable. self::$requireslockdebug = !READ_ONLY_SESSION; } self::start_session($requireslock); } /** * Handles starting a session. * * @param bool $requireslock If this is false then no write lock will be acquired, * and the session will be read-only. */ private static function start_session(bool $requireslock) { global $PERF, $CFG; try { self::$handler->init(); self::$handler->set_requires_write_lock($requireslock); self::prepare_cookies(); $isnewsession = empty($_COOKIE[session_name()]); if (!self::$handler->start()) { // Could not successfully start/recover session. throw new \core\session\exception('sessionstarterror', 'error'); } if ($requireslock) { // Grab the time when session lock starts. $PERF->sessionlock['gained'] = microtime(true); $PERF->sessionlock['wait'] = $PERF->sessionlock['gained'] - $PERF->sessionlock['start']; } self::initialise_user_session($isnewsession); self::$sessionactive = true; // Set here, so the session can be cleared if the security check fails. self::check_security(); if (!$requireslock || !self::$requireslockdebug) { self::$priorsession = (array) $_SESSION['SESSION']; } if (!empty($CFG->enable_read_only_sessions) && isset($_SESSION['SESSION']->cachestore_session)) { $caches = join(', ', array_keys($_SESSION['SESSION']->cachestore_session)); $caches = str_replace('default_session-', '', $caches); throw new \moodle_exception("The session caches can not be in the session store when " . "enable_read_only_sessions is enabled. Please map all session mode caches to be outside of the " . "default session store before enabling this features. Found these definitions in the session: $caches"); } // Link global $USER and $SESSION, // this is tricky because PHP does not allow references to references // and global keyword uses internally once reference to the $GLOBALS array. // The solution is to use the $GLOBALS['USER'] and $GLOBALS['$SESSION'] // as the main storage of data and put references to $_SESSION. $GLOBALS['USER'] = $_SESSION['USER']; $_SESSION['USER'] =& $GLOBALS['USER']; $GLOBALS['SESSION'] = $_SESSION['SESSION']; $_SESSION['SESSION'] =& $GLOBALS['SESSION']; } catch (\Exception $ex) { self::init_empty_session(); self::$sessionactive = false; throw $ex; } } /** * Returns current page performance info. * * @return array perf info */ public static function get_performance_info() { global $CFG, $PERF; if (!session_id()) { return array(); } self::load_handler(); $size = display_size(strlen(session_encode())); $handler = get_class(self::$handler); $info = array(); $info['size'] = $size; $info['html'] = html_writer::div("Session ($handler): $size", "sessionsize"); $info['txt'] = "Session ($handler): $size "; if (!empty($CFG->debugsessionlock)) { $sessionlock = self::get_session_lock_info(); if (!empty($sessionlock['held'])) { // The page displays the footer and the session has been closed. $sessionlocktext = "Session lock held: ".number_format($sessionlock['held'], 3)." secs"; } else { // The session hasn't yet been closed and so we assume now with microtime. $sessionlockheld = microtime(true) - $PERF->sessionlock['gained']; $sessionlocktext = "Session lock open: ".number_format($sessionlockheld, 3)." secs"; } $info['txt'] .= $sessionlocktext; $info['html'] .= html_writer::div($sessionlocktext, "sessionlockstart"); $sessionlockwaittext = "Session lock wait: ".number_format($sessionlock['wait'], 3)." secs"; $info['txt'] .= $sessionlockwaittext; $info['html'] .= html_writer::div($sessionlockwaittext, "sessionlockwait"); } return $info; } /** * Get fully qualified name of session handler class. * * @return string The name of the handler class */ public static function get_handler_class() { global $CFG, $DB; if (PHPUNIT_TEST) { return \core\tests\session\mock_handler::class; } else if (!empty($CFG->session_handler_class)) { return $CFG->session_handler_class; } else if (!empty($CFG->dbsessions) && $DB->session_lock_supported()) { return database::class; } return file::class; } /** * Create handler instance. */ protected static function load_handler() { if (self::$handler) { return; } // Find out which handler to use. $class = self::get_handler_class(); self::$handler = new $class(); if (!self::$handler instanceof \core\session\handler) { throw new exception("$class must implement the \core\session\handler"); } } /** * Empty current session, fill it with not-logged-in user info. * * This is intended for installation scripts, unit tests and other * special areas. Do NOT use for logout and session termination * in normal requests! * * @param mixed $newsid only used after initialising a user session, is this a new user session? */ public static function init_empty_session(?bool $newsid = null) { global $CFG; if (isset($GLOBALS['SESSION']->notifications)) { // Backup notifications. These should be preserved across session changes until the user fetches and clears them. $notifications = $GLOBALS['SESSION']->notifications; } $GLOBALS['SESSION'] = new \stdClass(); if (isset($newsid)) { $GLOBALS['SESSION']->isnewsessioncookie = $newsid; } $GLOBALS['USER'] = new \stdClass(); $GLOBALS['USER']->id = 0; if (!empty($notifications)) { // Restore notifications. $GLOBALS['SESSION']->notifications = $notifications; } if (isset($CFG->mnet_localhost_id)) { $GLOBALS['USER']->mnethostid = $CFG->mnet_localhost_id; } else { // Not installed yet, the future host id will be most probably 1. $GLOBALS['USER']->mnethostid = 1; } // Link global $USER and $SESSION. $_SESSION = array(); $_SESSION['USER'] =& $GLOBALS['USER']; $_SESSION['SESSION'] =& $GLOBALS['SESSION']; } /** * Make sure all cookie and session related stuff is configured properly before session start. */ protected static function prepare_cookies() { global $CFG; $cookiesecure = is_moodle_cookie_secure(); if (!isset($CFG->cookiehttponly)) { $CFG->cookiehttponly = 1; } // Set sessioncookie variable if it isn't already. if (!isset($CFG->sessioncookie)) { $CFG->sessioncookie = ''; } $sessionname = 'MoodleSession'.$CFG->sessioncookie; // Make sure cookie domain makes sense for this wwwroot. if (!isset($CFG->sessioncookiedomain)) { $CFG->sessioncookiedomain = ''; } else if ($CFG->sessioncookiedomain !== '') { $host = parse_url($CFG->wwwroot, PHP_URL_HOST); if ($CFG->sessioncookiedomain !== $host) { if (substr($CFG->sessioncookiedomain, 0, 1) === '.') { if (!preg_match('|^.*'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) { // Invalid domain - it must be end part of host. $CFG->sessioncookiedomain = ''; } } else { if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) { // Invalid domain - it must be end part of host. $CFG->sessioncookiedomain = ''; } } } } // Make sure the cookiepath is valid for this wwwroot or autodetect if not specified. if (!isset($CFG->sessioncookiepath)) { $CFG->sessioncookiepath = ''; } if ($CFG->sessioncookiepath !== '/') { $path = parse_url($CFG->wwwroot, PHP_URL_PATH).'/'; if ($CFG->sessioncookiepath === '') { $CFG->sessioncookiepath = $path; } else { if (strpos($path, $CFG->sessioncookiepath) !== 0 or substr($CFG->sessioncookiepath, -1) !== '/') { $CFG->sessioncookiepath = $path; } } } // Discard session ID from POST, GET and globals to tighten security, // this is session fixation prevention. unset($GLOBALS[$sessionname]); unset($_GET[$sessionname]); unset($_POST[$sessionname]); unset($_REQUEST[$sessionname]); // Compatibility hack for non-browser access to our web interface. if (!empty($_COOKIE[$sessionname]) && $_COOKIE[$sessionname] == "deleted") { unset($_COOKIE[$sessionname]); } // Set configuration. session_name($sessionname); $sessionoptions = [ 'lifetime' => 0, 'path' => $CFG->sessioncookiepath, 'domain' => $CFG->sessioncookiedomain, 'secure' => $cookiesecure, 'httponly' => $CFG->cookiehttponly, ]; if (self::should_use_samesite_none()) { // If $samesite is empty, we don't want there to be any SameSite attribute. $sessionoptions['samesite'] = 'None'; } session_set_cookie_params($sessionoptions); ini_set('session.use_trans_sid', '0'); ini_set('session.use_only_cookies', '1'); ini_set('session.use_strict_mode', '0'); // We have custom protection in session init. ini_set('session.serialize_handler', 'php'); // We can move to 'php_serialize' after we require PHP 5.5.4 form Moodle. // Moodle does normal session timeouts, this is for leftovers only. ini_set('session.gc_probability', 1); ini_set('session.gc_divisor', 1000); ini_set('session.gc_maxlifetime', 60*60*24*4); } /** * Initialise $_SESSION, handles google access * and sets up not-logged-in user properly. * * WARNING: $USER and $SESSION are set up later, do not use them yet! * * @param bool $newsid is this a new session in first http request? */ protected static function initialise_user_session($newsid) { global $CFG; $sid = session_id(); if (!$sid) { // No session, very weird. error_log('Missing session ID, session not started!'); self::init_empty_session($newsid); return; } $record = self::get_session_by_sid($sid); if (!isset($record->sid)) { if (!$newsid) { if (!empty($_SESSION['USER']->id)) { // This should not happen, just log it, we MUST not produce any output here! error_log("Cannot find session record $sid for user ".$_SESSION['USER']->id.", creating new session."); } // Prevent session fixation attacks. session_regenerate_id(true); } $_SESSION = array(); } unset($sid); if (isset($_SESSION['USER']->id)) { if (!empty($_SESSION['USER']->realuser)) { $userid = $_SESSION['USER']->realuser; } else { $userid = $_SESSION['USER']->id; } // Verify timeout first. $maxlifetime = $CFG->sessiontimeout; $timeout = false; if (isguestuser($userid) or empty($userid)) { // Ignore guest and not-logged in timeouts, there is very little risk here. $timeout = false; } else if ($record->timemodified < di::get(clock::class)->time() - $maxlifetime) { $timeout = true; $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. foreach ($authsequence as $authname) { $authplugin = get_auth_plugin($authname); if ($authplugin->ignore_timeout_hook($_SESSION['USER'], $record->sid, $record->timecreated, $record->timemodified)) { $timeout = false; break; } } } if ($timeout) { if (defined('NO_SESSION_UPDATE') && NO_SESSION_UPDATE) { return; } session_regenerate_id(true); $_SESSION = array(); self::destroy($record->sid); } else { // Update session tracking record. $update = new \stdClass(); $updated = false; if ($record->userid != $userid) { $update->userid = $record->userid = $userid; $updated = true; } $ip = getremoteaddr(); if ($record->lastip != $ip) { $update->lastip = $record->lastip = $ip; $updated = true; } $time = di::get(clock::class)->time(); $updatefreq = empty($CFG->session_update_timemodified_frequency) ? 20 : $CFG->session_update_timemodified_frequency; if ($record->timemodified == $record->timecreated) { // Always do first update of existing record. $update->timemodified = $record->timemodified = $time; $updated = true; } else if ($record->timemodified < $time - $updatefreq) { // Update the session modified flag only once every 20 seconds. $update->timemodified = $record->timemodified = $time; $updated = true; } if ($updated && (!defined('NO_SESSION_UPDATE') || !NO_SESSION_UPDATE)) { $update->id = $record->id; $update->userid = $record->userid; self::$handler->update_session($update); } return; } } else if (isset($record->sid)) { // This happens when people switch session handlers... session_regenerate_id(true); $_SESSION = []; self::destroy($record->sid); } unset($record); $timedout = false; if (!isset($_SESSION['SESSION'])) { $_SESSION['SESSION'] = new \stdClass(); if (!$newsid) { $timedout = true; } } $user = null; if (!empty($CFG->opentowebcrawlers)) { if (\core_useragent::is_web_crawler()) { $user = guest_user(); } $referer = get_local_referer(false); if (!empty($CFG->guestloginbutton) and !$user and !empty($referer)) { // Automatically log in users coming from search engine results. if (strpos($referer, 'google') !== false ) { $user = guest_user(); } else if (strpos($referer, 'altavista') !== false ) { $user = guest_user(); } } } // Setup $USER and insert the session tracking record. if ($user) { self::set_user($user); self::add_session($user->id); } else { self::init_empty_session($newsid); self::add_session(0); } if ($timedout) { $_SESSION['SESSION']->has_timed_out = true; } } /** * Returns a single session record for this session id. * * @param string $sid * @return \stdClass */ public static function get_session_by_sid(string $sid): \stdClass { return self::$handler->get_session_by_sid($sid); } /** * Returns all the session records for this user id. * * @param int $userid * @return array */ public static function get_sessions_by_userid(int $userid): array { return self::$handler->get_sessions_by_userid($userid); } /** * Insert new empty session record. * * @param int $userid * @return \stdClass the new record */ public static function add_session(int $userid): \stdClass { return self::$handler->add_session($userid); } /** * Update a session record. * * @param \stdClass $record * @return bool */ public static function update_session(\stdClass $record): bool { return self::$handler->update_session($record); } /** * Do various session security checks. * * WARNING: $USER and $SESSION are set up later, do not use them yet! * @throws \core\session\exception */ protected static function check_security() { global $CFG; if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) { // Make sure current IP matches the one for this session. $remoteaddr = getremoteaddr(); if (empty($_SESSION['USER']->sessionip)) { $_SESSION['USER']->sessionip = $remoteaddr; } if ($_SESSION['USER']->sessionip != $remoteaddr) { // This is a security feature - terminate the session in case of any doubt. self::terminate_current(); throw new exception('sessionipnomatch2', 'error'); } } } /** * Login user, to be called from complete_user_login() only. * @param \stdClass $user */ public static function login_user(\stdClass $user) { global $DB; // Regenerate session id and delete old session, // this helps prevent session fixation attacks from the same domain. $sid = session_id(); session_regenerate_id(true); self::destroy($sid); self::add_session($user->id); // Let enrol plugins deal with new enrolments if necessary. enrol_check_plugins($user); // Setup $USER object. self::set_user($user); } /** * Returns a valid setting for the SameSite cookie attribute. * * @return string The desired setting for the SameSite attribute on the cookie. Empty string indicates the SameSite attribute * should not be set at all. */ private static function should_use_samesite_none(): bool { // We only want None or no attribute at this point. When we have cookie handling compatible with Lax, // we can look at checking a setting. // Browser support for none is not consistent yet. There are known issues with Safari, and IE11. // Things are stablising, however as they're not stable yet we will deal specifically with the version of chrome // that introduces a default of lax, setting it to none for the current version of chrome (2 releases before the change). // We also check you are using secure cookies and HTTPS because if you are not running over HTTPS // then setting SameSite=None will cause your session cookie to be rejected. if (\core_useragent::is_chrome() && \core_useragent::check_chrome_version('78') && is_moodle_cookie_secure()) { return true; } return false; } /** * Terminate current user session. * @return void */ public static function terminate_current() { global $DB; if (!self::$sessionactive) { self::init_empty_session(); self::$sessionactive = false; return; } try { $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED)); } catch (\Exception $ignored) { // Probably install/upgrade - ignore this problem. } // Initialize variable to pass-by-reference to headers_sent(&$file, &$line). $file = null; $line = null; if (headers_sent($file, $line)) { error_log('Cannot terminate session properly - headers were already sent in file: '.$file.' on line '.$line); } // Write new empty session and make sure the old one is deleted. $sid = session_id(); session_regenerate_id(true); self::destroy($sid); self::init_empty_session(); self::add_session($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet. self::write_close(); } /** * No more changes in session expected. * Unblocks the sessions, other scripts may start executing in parallel. */ public static function write_close() { global $PERF, $ME, $CFG; if (self::$sessionactive) { $requireslock = self::$handler->requires_write_lock(); if ($requireslock) { // Grab the time when session lock is released. $PERF->sessionlock['released'] = microtime(true); if (!empty($PERF->sessionlock['gained'])) { $PERF->sessionlock['held'] = $PERF->sessionlock['released'] - $PERF->sessionlock['gained']; } $PERF->sessionlock['url'] = me(); self::update_recent_session_locks($PERF->sessionlock); self::sessionlock_debugging(); } // If debugging, take a snapshot of session at close and compare on shutdown to detect any accidental mutations. if (debugging()) { self::$sessionatclose = (array) $_SESSION['SESSION']; \core_shutdown_manager::register_function('\core\session\manager::check_mutated_closed_session'); } if (!$requireslock || !self::$requireslockdebug) { // Compare the array of the earlier session data with the array now, if // there is a difference then a lock is required. $arraydiff = self::array_session_diff( self::$priorsession, (array) $_SESSION['SESSION'] ); if ($arraydiff) { $error = "Script $ME defined READ_ONLY_SESSION but the following SESSION attributes were changed:"; foreach ($arraydiff as $key => $value) { $error .= ' $SESSION->' . $key; } // This will emit an error if debugging is on, even if $CFG->enable_read_only_sessions // is not true as we need to surface this class of errors. error_log($error); // phpcs:ignore } } } // More control over whether session data // is persisted or not. if (self::$sessionactive && session_id()) { // Write session and release lock only if // indication session start was clean. self::$handler->write_close(); } else { // Otherwise, if possible lock exists want // to clear it, but do not write session. // If the $handler has not been set then // there is no session to abort. if (isset(self::$handler)) { @self::$handler->abort(); } } self::$sessionactive = false; } /** * Checks if the session has been mutated since it was closed. * In write_close the session is saved to the variable $sessionatclose * If there is a difference between $sessionatclose and the current session, * it means a script has erroneously closed the session too early. * Script is usually called in shutdown_manager */ public static function check_mutated_closed_session() { global $ME; // Session is still open, mutations are allowed. if (self::$sessionactive) { return; } // Detect if session was cleared. if (!isset($_SESSION['SESSION']) && isset(self::$sessionatclose)) { debugging("Script $ME cleared the session after it was closed."); return; } else if (!isset($_SESSION['SESSION'])) { // Else session is empty, nothing to check. return; } // Session is closed - compare the current session to the session when write_close was called. $arraydiff = self::array_session_diff( self::$sessionatclose, (array) $_SESSION['SESSION'] ); if ($arraydiff) { $error = "Script $ME mutated the session after it was closed:"; foreach ($arraydiff as $key => $value) { $error .= ' $SESSION->' . $key; // Extra debugging for cachestore session changes. if (strpos($key, 'cachestore_') === 0 && is_array($value)) { $error .= ': ' . implode(',', array_keys($value)); } } debugging($error); } } /** * Does the PHP session with given id exist? * * The session must exist both in session table and actual * session backend and the session must not be timed out. * * Timeout evaluation is simplified, the auth hooks are not executed. * * @param string $sid * @return bool */ public static function session_exists($sid) { global $DB, $CFG; if (empty($CFG->version)) { // Not installed yet, do not try to access database. return false; } // Note: add sessions->state checking here if it gets implemented. $record = self::get_session_by_sid($sid); if (!isset($record->sid)) { return false; } if (empty($record->userid) or isguestuser($record->userid)) { // Ignore guest and not-logged-in timeouts, there is very little risk here. } else if ($record->timemodified < di::get(clock::class)->time() - $CFG->sessiontimeout) { return false; } // There is no need the existence of handler storage in public API. self::load_handler(); return self::$handler->session_exists($sid); } /** * Return the number of seconds remaining in the current session. * @param string $sid */ public static function time_remaining($sid) { global $DB, $CFG; if (empty($CFG->version)) { // Not installed yet, do not try to access database. return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout]; } // Note: add sessions->state checking here if it gets implemented. if (!$record = self::get_session_by_sid($sid)) { return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout]; } if (empty($record->userid) or isguestuser($record->userid)) { // Ignore guest and not-logged-in timeouts, there is very little risk here. return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout]; } else { return [ 'userid' => $record->userid, 'timeremaining' => $CFG->sessiontimeout - (di::get(clock::class)->time() - $record->timemodified), ]; } } /** * Fake last access for given session, this prevents session timeout. * @param string $sid */ public static function touch_session($sid) { // Timeouts depend on core sessions table only, no need to update anything in external stores. self::$handler->update_session((object) [ 'sid' => $sid, 'timemodified' => di::get(clock::class)->time(), ]); } /** * Terminate all sessions unconditionally. * * @return void * @deprecated since Moodle 4.5 See MDL-66161 * @todo Remove in MDL-81848 */ #[\core\attribute\deprecated( replacement: 'destroy_all', since: '4.5', )] public static function kill_all_sessions(): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); self::destroy_all(); } /** * Terminate give session unconditionally. * * @param string $sid * @return void * @deprecated since Moodle 4.5 See MDL-66161 * @todo Remove in MDL-81848 */ #[\core\attribute\deprecated( replacement: 'destroy', since: '4.5', )] public static function kill_session($sid): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); self::destroy($sid); } /** * Kill sessions of users with disabled plugins. * * @param string $pluginname * @return void * @deprecated since Moodle 4.5 See MDL-66161 * @todo Remove in MDL-81848 */ #[\core\attribute\deprecated( replacement: 'destroy_by_auth_plugin', since: '4.5', )] public static function kill_sessions_for_auth_plugin(string $pluginname): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); self::destroy_by_auth_plugin($pluginname); } /** * Terminate all sessions of given user unconditionally. * * @param int $userid * @param string $keepsid keep this sid if present * @deprecated since Moodle 4.5 See MDL-66161 * @todo Remove in MDL-81848 */ #[\core\attribute\deprecated( replacement: 'destroy_user_sessions', since: '4.5', )] public static function kill_user_sessions($userid, $keepsid = null) { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); self::destroy_user_sessions($userid, $keepsid); } /** * Destroy all sessions for a given plugin. * Typically used when a plugin is disabled or uninstalled, so all sessions (users) for that plugin are logged out. * * @param string $pluginname Auth plugin name. */ public static function destroy_by_auth_plugin(string $pluginname): void { self::$handler->destroy_by_auth_plugin($pluginname); } /** * Destroy all sessions, and delete all the session data. * * @return bool */ public static function destroy_all(): bool { self::terminate_current(); self::load_handler(); try { $result = self::$handler->destroy_all(); } catch (\moodle_exception $ignored) { // Do not show any warnings - might be during upgrade/installation. $result = true; } return $result; } /** * Destroy a specific session and delete this session record for this session id. * * @param string $id * @return bool */ public static function destroy(string $id): bool { self::load_handler(); if ($id === session_id()) { self::write_close(); } return self::$handler->destroy($id); } /** * Destroy all sessions of given user unconditionally. * @param int $userid * @param string $keepsid keep this sid if present */ public static function destroy_user_sessions($userid, $keepsid = null) { $sessions = self::get_sessions_by_userid($userid); foreach ($sessions as $session) { if ($keepsid and $keepsid === $session->sid) { continue; } self::destroy($session->sid); } } /** * Terminate other sessions of current user depending * on $CFG->limitconcurrentlogins restriction. * * This is expected to be called right after complete_user_login(). * * NOTE: * * Do not use from SSO auth plugins, this would not work. * * Do not use from web services because they do not have sessions. * * @param int $userid * @param string $sid session id to be always keep, usually the current one * @return void */ public static function apply_concurrent_login_limit($userid, $sid = null) { global $CFG, $DB; // NOTE: the $sid parameter is here mainly to allow testing, // in most cases it should be current session id. if (isguestuser($userid) or empty($userid)) { // This applies to real users only! return; } if (empty($CFG->limitconcurrentlogins) or $CFG->limitconcurrentlogins < 0) { return; } $sessions = self::get_sessions_by_userid($userid); $count = count($sessions); if ($count <= $CFG->limitconcurrentlogins) { return; } $i = 0; if ($sid) { foreach ($sessions as $key => $session) { if ($session->sid == $sid && $session->userid == $userid) { $i = 1; unset($sessions[$key]); } } } // Order records by timecreated DESC. usort($sessions, function($a, $b){ return $b->timecreated <=> $a->timecreated; }); foreach ($sessions as $session) { $i++; if ($i <= $CFG->limitconcurrentlogins) { continue; } self::destroy($session->sid); } } /** * Set current user. * * @param \stdClass $user record */ public static function set_user(\stdClass $user) { global $ADMIN; $GLOBALS['USER'] = $user; unset($GLOBALS['USER']->description); // Conserve memory. unset($GLOBALS['USER']->password); // Improve security. if (isset($GLOBALS['USER']->lang)) { // Make sure it is a valid lang pack name. $GLOBALS['USER']->lang = clean_param($GLOBALS['USER']->lang, PARAM_LANG); } // Relink session with global $USER just in case it got unlinked somehow. $_SESSION['USER'] =& $GLOBALS['USER']; // Nullify the $ADMIN tree global. If we're changing users, then this is now stale and must be generated again if needed. $ADMIN = null; // Init session key. sesskey(); // Make sure the user is correct in web server access logs. set_access_log_user(); } /** * Periodic timed-out session cleanup. * * @param int $maxlifetime Sessions that have not updated for the last max_lifetime seconds will be removed. * @return void */ public static function gc(int $maxlifetime = 0): void { global $CFG; // If max lifetime is not provided, use the default session timeout. if ($maxlifetime == 0) { $maxlifetime = $CFG->sessiontimeout; } self::$handler->gc($maxlifetime); } /** * Is current $USER logged-in-as somebody else? * @return bool */ public static function is_loggedinas() { return !empty($GLOBALS['USER']->realuser); } /** * Returns the $USER object ignoring current login-as session * @return \stdClass user object */ public static function get_realuser() { if (self::is_loggedinas()) { return $_SESSION['REALUSER']; } else { return $GLOBALS['USER']; } } /** * Login as another user - no security checks here. * @param int $userid * @param \context $context * @param bool $generateevent Set to false to prevent the loginas event to be generated * @return void */ public static function loginas($userid, \context $context, $generateevent = true) { global $USER; if (self::is_loggedinas()) { return; } // Switch to fresh new $_SESSION. $_SESSION = array(); $_SESSION['REALSESSION'] = clone($GLOBALS['SESSION']); $GLOBALS['SESSION'] = new \stdClass(); $_SESSION['SESSION'] =& $GLOBALS['SESSION']; // Create the new $USER object with all details and reload needed capabilities. $_SESSION['REALUSER'] = clone($GLOBALS['USER']); $user = get_complete_user_data('id', $userid); $user->realuser = $_SESSION['REALUSER']->id; $user->loginascontext = $context; // Let enrol plugins deal with new enrolments if necessary. enrol_check_plugins($user); if ($generateevent) { // Create event before $USER is updated. $event = \core\event\user_loggedinas::create( array( 'objectid' => $USER->id, 'context' => $context, 'relateduserid' => $userid, 'other' => array( 'originalusername' => fullname($USER, true), 'loggedinasusername' => fullname($user, true) ) ) ); } // Set up global $USER. \core\session\manager::set_user($user); if ($generateevent) { $event->trigger(); } // Queue migrating the messaging data, if we need to. if (!get_user_preferences('core_message_migrate_data', false, $userid)) { // Check if there are any legacy messages to migrate. if (\core_message\helper::legacy_messages_exist($userid)) { \core_message\task\migrate_message_data::queue_task($userid); } else { set_user_preference('core_message_migrate_data', true, $userid); } } } /** * Add a JS session keepalive to the page. * * A JS session keepalive script will be called to update the session modification time every $frequency seconds. * * Upon failure, the specified error message will be shown to the user. * * @param string $identifier The string identifier for the message to show on failure. * @param string $component The string component for the message to show on failure. * @param int $frequency The update frequency in seconds. * @param int $timeout The timeout of each request in seconds. * @throws \coding_exception IF the frequency is longer than the session lifetime. */ public static function keepalive($identifier = 'sessionerroruser', $component = 'error', $frequency = null, $timeout = 0) { global $CFG, $PAGE; if ($frequency) { if ($frequency > $CFG->sessiontimeout) { // Sanity check the frequency. throw new \coding_exception('Keepalive frequency is longer than the session lifespan.'); } } else { // A frequency of sessiontimeout / 10 matches the timeouts in core/network amd module. $frequency = $CFG->sessiontimeout / 10; } $PAGE->requires->js_call_amd('core/network', 'keepalive', array( $frequency, $timeout, $identifier, $component )); } /** * Generate a new login token and store it in the session. * * @return array The current login state. */ private static function create_login_token() { global $SESSION; $state = [ 'token' => random_string(32), 'created' => di::get(clock::class)->time(), // Server time - not user time. ]; if (!isset($SESSION->logintoken)) { $SESSION->logintoken = []; } // Overwrite any previous values. $SESSION->logintoken[self::$logintokenkey] = $state; return $state; } /** * Get the current login token or generate a new one. * * All login forms generated from Moodle must include a login token * named "logintoken" with the value being the result of this function. * Logins will be rejected if they do not include this token as well as * the username and password fields. * * @return string The current login token. */ public static function get_login_token() { global $CFG, $SESSION; $state = false; if (!isset($SESSION->logintoken)) { $SESSION->logintoken = []; } if (array_key_exists(self::$logintokenkey, $SESSION->logintoken)) { $state = $SESSION->logintoken[self::$logintokenkey]; } if (empty($state)) { $state = self::create_login_token(); } // Check token lifespan. if ($state['created'] < (di::get(clock::class)->time() - $CFG->sessiontimeout)) { $state = self::create_login_token(); } // Return the current session login token. if (array_key_exists('token', $state)) { return $state['token']; } else { return false; } } /** * Check the submitted value against the stored login token. * * @param mixed $token The value submitted in the login form that we are validating. * If false is passed for the token, this function will always return true. * @return boolean If the submitted token is valid. */ public static function validate_login_token($token = false) { global $CFG, $SESSION; if (!empty($CFG->alternateloginurl) || !empty($CFG->disablelogintoken)) { // An external login page cannot generate the login token we need to protect CSRF on // login requests. // Other custom login workflows may skip this check by setting disablelogintoken in config. return true; } if ($token === false) { // authenticate_user_login is a core function was extended to validate tokens. // For existing uses other than the login form it does not // validate that a token was generated. // Some uses that do not validate the token are login/token.php, // or an auth plugin like auth/ldap/auth.php. return true; } $currenttoken = self::get_login_token(); // We need to clean the login token so the old one is not valid again. unset($SESSION->logintoken); if ($currenttoken !== $token) { // Fail the login. return false; } return true; } /** * Get the recent session locks array. * * @return array Recent session locks array. */ public static function get_recent_session_locks() { global $SESSION; if (!isset($SESSION->recentsessionlocks)) { // This will hold the pages that blocks other page. $SESSION->recentsessionlocks = array(); } return $SESSION->recentsessionlocks; } /** * Updates the recent session locks. * * This function will store session lock info of all the pages visited. * * @param array $sessionlock Session lock array. */ public static function update_recent_session_locks($sessionlock) { global $CFG, $SESSION; if (empty($CFG->debugsessionlock)) { return; } $readonlysession = defined('READ_ONLY_SESSION') && READ_ONLY_SESSION; $readonlydebugging = !empty($CFG->enable_read_only_sessions) || !empty($CFG->enable_read_only_sessions_debug); if ($readonlysession && $readonlydebugging) { return; } $SESSION->recentsessionlocks = self::get_recent_session_locks(); array_push($SESSION->recentsessionlocks, $sessionlock); self::cleanup_recent_session_locks(); } /** * Reset recent session locks array if there is a time gap more than SESSION_RESET_GAP_THRESHOLD. */ public static function cleanup_recent_session_locks() { global $SESSION; $locks = self::get_recent_session_locks(); if (count($locks) > self::MAXIMUM_STORED_SESSION_HISTORY) { // Keep the last MAXIMUM_STORED_SESSION_HISTORY locks and ignore the rest. $locks = array_slice($locks, -1 * self::MAXIMUM_STORED_SESSION_HISTORY); } if (count($locks) > 2) { for ($i = count($locks) - 1; $i > 0; $i--) { // Calculate the gap between session locks. $gap = $locks[$i]['released'] - $locks[$i - 1]['start']; if ($gap >= self::SESSION_RESET_GAP_THRESHOLD) { // Remove previous locks if the gap is 1 second or more. $SESSION->recentsessionlocks = array_slice($locks, $i); break; } } } } /** * Get the page that blocks other pages at a specific timestamp. * * Look for a page whose lock was gained before that timestamp, and released after that timestamp. * * @param float $time Time before session lock starts. * @return array|null */ public static function get_locked_page_at($time) { $recentsessionlocks = self::get_recent_session_locks(); foreach ($recentsessionlocks as $recentsessionlock) { if ($time >= $recentsessionlock['gained'] && $time <= $recentsessionlock['released']) { return $recentsessionlock; } } } /** * Display the page which blocks other pages. * * @return string */ public static function display_blocking_page() { global $PERF; $page = self::get_locked_page_at($PERF->sessionlock['start']); $output = "Script ".me()." was blocked for "; $output .= number_format($PERF->sessionlock['wait'], 3); if ($page != null) { $output .= " second(s) by script: "; $output .= $page['url']; } else { $output .= " second(s) by an unknown script."; } return $output; } /** * Get session lock info of the current page. * * @return array */ public static function get_session_lock_info() { global $PERF; if (!isset($PERF->sessionlock)) { return null; } return $PERF->sessionlock; } /** * Display debugging info about slow and blocked script. */ public static function sessionlock_debugging() { global $CFG, $PERF; if (!empty($CFG->debugsessionlock)) { if (isset($PERF->sessionlock['held']) && $PERF->sessionlock['held'] > $CFG->debugsessionlock) { debugging("Script ".me()." locked the session for ".number_format($PERF->sessionlock['held'], 3) ." seconds, it should close the session using \core\session\manager::write_close().", DEBUG_NORMAL); } if (isset($PERF->sessionlock['wait']) && $PERF->sessionlock['wait'] > $CFG->debugsessionlock) { $output = self::display_blocking_page(); debugging($output, DEBUG_DEVELOPER); } } } /** * Compares two arrays and outputs the difference. * * Note - checking between objects and array type is only done at the top level. * Any changes in types below the top level will not be detected. * However, if their values are the same, they will be treated as equal. * * Any changes, such as removals, edits or additions will be detected. * * @param array $previous * @param array $current * @return array */ private static function array_session_diff(array $previous, array $current): array { // To use array_udiff_uassoc, the first array must have the most keys; this ensures every key is checked. // To do this, we first need to sort them by the length of their keys. $arrays = [$current, $previous]; // Sort them by the length of their keys. usort($arrays, function ($a, $b) { return count(array_keys($b)) - count(array_keys($a)); }); // The largest is the first value in the $arrays, after sorting. // The smallest is then the last one. // If they are the same size, it does not matter which is which. $largest = $arrays[0]; $smallest = $arrays[1]; // Defines a function that casts the values to arrays. // This is so the properties are compared, instead any object's identities. $casttoarray = function ($value) { return json_decode(json_encode($value), true); }; // Defines a function that compares all keys by their string value. $keycompare = function ($a, $b) { return strcmp($a, $b); }; // Defines a function that compares all values by first their type, and then their values. // If the value contains any objects, they are cast to arrays before comparison. $valcompare = function ($a, $b) use ($casttoarray) { // First compare type. // If they are not the same type, they are definitely not the same. // Note we do not check types recursively. if (gettype($a) !== gettype($b)) { return 1; } // Next compare value. Cast any objects to arrays to compare their properties, // instead of the identitiy of the object itself. $v1 = $casttoarray($a); $v2 = $casttoarray($b); if ($v1 !== $v2) { return 1; } return 0; }; // Apply the comparison functions to the two given session arrays, // making sure to use the largest array first, so that all keys are considered. return array_udiff_uassoc($largest, $smallest, $valcompare, $keycompare); } } PK"\(E'E' handler.phpnu[. namespace core\session; use core\clock; use core\di; use stdClass; /** * Session handler base. * * @package core * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class handler { /** @var boolean $requireswritelock does the session need and/or have a lock? */ protected $requireswritelock = false; /** * Start the session. * @return bool success */ public function start() { return session_start(); } /** * Write the session and release lock. If the session was not intentionally opened * with a write lock, then we will abort the session instead if able. */ public function write_close() { if ($this->requires_write_lock()) { session_write_close(); $this->requireswritelock = false; } else { $this->abort(); } } /** * Release lock on the session without writing it. * May not be possible in older versions of PHP. If so, session may be written anyway * so that any locks are released. */ public function abort() { session_abort(); $this->requireswritelock = false; } /** * This is called after init() and before start() to indicate whether the session * opened should be writable or not. This is intentionally captured even if your * handler doesn't support non-locking sessions, so that behavior (upon session close) * matches closely between handlers. * @param bool $requireswritelock true if needs to be open for writing */ public function set_requires_write_lock($requireswritelock) { $this->requireswritelock = $requireswritelock; } /** * Returns all session records. * * @return \Iterator */ public function get_all_sessions(): \Iterator { global $DB; $rs = $DB->get_recordset('sessions'); foreach ($rs as $row) { yield $row; } $rs->close(); } /** * Returns a single session record for this session id. * * @param string $sid * @return stdClass */ public function get_session_by_sid(string $sid): stdClass { global $DB; return $DB->get_record('sessions', ['sid' => $sid]) ?: new stdClass(); } /** * Returns all the session records for this user id. * * @param int $userid * @return array */ public function get_sessions_by_userid(int $userid): array { global $DB; return $DB->get_records('sessions', ['userid' => $userid]); } /** * Insert new empty session record. * * @param int $userid * @return stdClass the new record */ public function add_session(int $userid): stdClass { global $DB; $record = new stdClass(); $record->state = 0; $record->sid = session_id(); $record->sessdata = null; $record->userid = $userid; $record->timecreated = $record->timemodified = di::get(clock::class)->time(); $record->firstip = $record->lastip = getremoteaddr(); $record->id = $DB->insert_record('sessions', $record); return $record; } /** * Update a session record. * * @param stdClass $record * @return bool */ public function update_session(stdClass $record): bool { global $DB; if (!isset($record->id) && isset($record->sid)) { $record->id = $DB->get_field('sessions', 'id', ['sid' => $record->sid]); } return $DB->update_record('sessions', $record); } /** * Destroy a specific session and delete this session record for this session id. * * @param string $id session id * @return bool */ public function destroy(string $id): bool { global $DB; return $DB->delete_records('sessions', ['sid' => $id]); } /** * Destroy all sessions, and delete all the session data. * * @return bool */ public function destroy_all(): bool { global $DB; return $DB->delete_records('sessions'); } /** * Clean up expired sessions. * * @param int $purgebefore Sessions that have not updated for the last purgebefore timestamp will be removed. * @param int $userid */ protected function destroy_expired_user_sessions(int $purgebefore, int $userid): void { $sessions = $this->get_sessions_by_userid($userid); foreach ($sessions as $session) { if ($session->timemodified < $purgebefore) { $this->destroy($session->sid); } } } /** * Clean up all expired sessions. * * @param int $purgebefore */ protected function destroy_all_expired_sessions(int $purgebefore): void { global $DB, $CFG; $authsequence = get_enabled_auth_plugins(); $authsequence = array_flip($authsequence); unset($authsequence['nologin']); // No login means user cannot login. $authsequence = array_flip($authsequence); $authplugins = []; foreach ($authsequence as $authname) { $authplugins[$authname] = get_auth_plugin($authname); } $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified FROM {user} u JOIN {sessions} s ON s.userid = u.id WHERE s.timemodified < :purgebefore AND u.id <> :guestid"; $params = ['purgebefore' => $purgebefore, 'guestid' => $CFG->siteguest]; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $user) { foreach ($authplugins as $authplugin) { if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) { continue 2; } } $this->destroy($user->sid); } $rs->close(); } /** * Destroy all sessions for a given plugin. * Typically used when a plugin is disabled or uninstalled, so all sessions (users) for that plugin are logged out. * * @param string $pluginname Auth plugin name. */ public function destroy_by_auth_plugin(string $pluginname): void { global $DB; $rs = $DB->get_recordset('user', ['auth' => $pluginname], 'id ASC', 'id'); foreach ($rs as $user) { $sessions = $this->get_sessions_by_userid($user->id); foreach ($sessions as $session) { $this->destroy($session->sid); } } $rs->close(); } // phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameUnderscore /** * Periodic timed-out session cleanup. * * @param int $max_lifetime Sessions that have not updated for the last max_lifetime seconds will be removed. * @return int|false Number of deleted sessions or false if an error occurred. */ public function gc(int $max_lifetime = 0): int|false { global $CFG; // This may take a long time. \core_php_time_limit::raise(); if ($max_lifetime === 0) { $max_lifetime = (int) $CFG->sessiontimeout; } try { // Calculate the timestamp before which sessions are considered expired. $purgebefore = di::get(clock::class)->time() - $max_lifetime; // Delete expired sessions for guest user account. $this->destroy_expired_user_sessions($purgebefore, $CFG->siteguest); // Delete expired sessions for userid = 0 (not logged in), better kill them asap to release memory. $this->destroy_expired_user_sessions($purgebefore, 0); // Clean up expired sessions for real users only. $this->destroy_all_expired_sessions($purgebefore); // Cleanup leftovers from the first browser access because it may set multiple cookies and then use only one. $purgebefore = di::get(clock::class)->time() - (60 * 3); $sessions = $this->get_sessions_by_userid(0); foreach ($sessions as $session) { if ($session->timemodified == $session->timecreated && $session->timemodified < $purgebefore) { $this->destroy($session->sid); } } } catch (\Exception $ex) { debugging('Error gc-ing sessions: '.$ex->getMessage(), DEBUG_NORMAL, $ex->getTrace()); } return 0; } // phpcs:enable /** * Has this session been opened with a writelock? Your handler should call this during * start() if you support read-only sessions. * @return bool true if session is intended to have a write lock. */ public function requires_write_lock() { return $this->requireswritelock; } /** * Init session handler. */ abstract public function init(); /** * Check the backend contains data for this session id. * * Note: this is intended to be called from manager::session_exists() only. * * @param string $sid * @return bool true if session found. */ abstract public function session_exists($sid); } PK"\aq redis.phpnu[. namespace core\session; use coding_exception; use core\di; use core\clock; use RedisCluster; use RedisClusterException; use RedisException; use SessionHandlerInterface; /** * Redis based session handler. * * @package core * @copyright Russell Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class redis extends handler implements SessionHandlerInterface { /** * Compressor: none. */ const COMPRESSION_NONE = 'none'; /** * Compressor: PHP GZip. */ const COMPRESSION_GZIP = 'gzip'; /** * Compressor: PHP Zstandard. */ const COMPRESSION_ZSTD = 'zstd'; /** @var string Minimum server version */ const REDIS_MIN_SERVER_VERSION = "5.0.0"; /** @var string Minimum extension version */ const REDIS_MIN_EXTENSION_VERSION = "5.1.0"; /** @var array $host save_path string */ protected array $host = []; /** @var int $port The port to connect to */ protected $port = 6379; /** @var array $sslopts SSL options, if applicable */ protected $sslopts = []; /** @var string $auth redis password */ protected $auth = ''; /** @var int $database the Redis database to store sesions in */ protected $database = 0; /** @var array $servers list of servers parsed from save_path */ protected $prefix = ''; /** @var string $sessionkeyprefix the prefix for the session key */ protected string $sessionkeyprefix = 'session_'; /** @var string $userkeyprefix the prefix for the user key */ protected string $userkeyprefix = 'user_'; /** @var int $acquiretimeout how long to wait for session lock in seconds */ protected $acquiretimeout = 120; /** @var int $acquirewarn how long before warning when waiting for a lock in seconds */ protected $acquirewarn = null; /** @var int $lockretry how long to wait between session lock attempts in ms */ protected $lockretry = 100; /** @var int $serializer The serializer to use */ protected $serializer = \Redis::SERIALIZER_PHP; /** @var int $compressor The compressor to use */ protected $compressor = self::COMPRESSION_NONE; /** @var string $lasthash hash of the session data content */ protected $lasthash = null; /** @var int $gcbatchsize The number of redis keys that will be processed each time the garbage collector is executed. */ protected int $gcbatchsize = 100; /** * How long to wait in seconds before expiring the lock automatically so that other requests may continue execution. * * @var int $lockexpire */ protected int $lockexpire; /** @var \Redis|\RedisCluster|null Connection */ protected \Redis|\RedisCluster|null $connection = null; /** @var array $locks List of currently held locks by this page. */ protected $locks = array(); /** @var int $timeout How long sessions live before expiring. */ protected $timeout; /** @var bool $clustermode Redis in cluster mode. */ protected bool $clustermode = false; /** @var int Maximum number of retries for cache store operations. */ protected int $maxretries = 3; /** @var int $firstaccesstimeout The initial timeout (seconds) for the first browser access without login. */ protected int $firstaccesstimeout = 180; /** @var clock A clock instance */ protected clock $clock; /** @var int $connectiontimeout The number of seconds to wait for a connection or response from the Redis server. */ protected int $connectiontimeout = 3; /** * Create new instance of handler. */ public function __construct() { global $CFG; if (isset($CFG->session_redis_host)) { // If there is only one host, use the single Redis connection. // If there are multiple hosts (separated by a comma), use the Redis cluster connection. $this->host = array_filter(array_map('trim', explode(',', $CFG->session_redis_host))); $this->clustermode = count($this->host) > 1; } if (isset($CFG->session_redis_port)) { $this->port = (int)$CFG->session_redis_port; } if (isset($CFG->session_redis_encrypt) && $CFG->session_redis_encrypt) { $this->sslopts = $CFG->session_redis_encrypt; } if (isset($CFG->session_redis_auth)) { $this->auth = $CFG->session_redis_auth; } if (isset($CFG->session_redis_database)) { $this->database = (int)$CFG->session_redis_database; } if (isset($CFG->session_redis_prefix)) { $this->prefix = $CFG->session_redis_prefix; } if (isset($CFG->session_redis_acquire_lock_timeout)) { $this->acquiretimeout = (int)$CFG->session_redis_acquire_lock_timeout; } if (isset($CFG->session_redis_acquire_lock_warn)) { $this->acquirewarn = (int)$CFG->session_redis_acquire_lock_warn; } if (isset($CFG->session_redis_acquire_lock_retry)) { $this->lockretry = (int)$CFG->session_redis_acquire_lock_retry; } if (!empty($CFG->session_redis_serializer_use_igbinary) && defined('\Redis::SERIALIZER_IGBINARY')) { $this->serializer = \Redis::SERIALIZER_IGBINARY; // Set igbinary serializer if phpredis supports it. } // This sets the Redis session lock expiry time to whatever is lower, either // the PHP execution time `max_execution_time`, if the value is positive, or the // globally configured `sessiontimeout`. // // Setting it to the lower of the two will not make things worse it if the execution timeout // is longer than the session timeout. // // For the PHP execution time, once the PHP execution time is over, we can be sure // that the lock is no longer actively held so that the lock can expire safely. // // Although at `lib/classes/php_time_limit.php::raise(int)`, Moodle can // progressively increase the maximum PHP execution time, this is limited to the // `max_execution_time` value defined in the `php.ini`. // For the session timeout, we assume it is safe to consider the lock to expire // once the session itself expires. // If we unnecessarily hold the lock any longer, it blocks other session requests. $this->lockexpire = ini_get('max_execution_time'); if ($this->lockexpire < 0) { // If the max_execution_time is set to a value lower than 0, which is invalid, use the default value. // https://www.php.net/manual/en/info.configuration.php#ini.max-execution-time defines the default as 30. // Note: This value is not available programatically. $this->lockexpire = 30; } if (empty($this->lockexpire) || ($this->lockexpire > (int)$CFG->sessiontimeout)) { // The value of the max_execution_time is either unlimited (0), or higher than the session timeout. // Cap it at the session timeout. $this->lockexpire = (int)$CFG->sessiontimeout; } if (isset($CFG->session_redis_lock_expire)) { $this->lockexpire = (int)$CFG->session_redis_lock_expire; } if (isset($CFG->session_redis_compressor)) { $this->compressor = $CFG->session_redis_compressor; } if (isset($CFG->session_redis_connection_timeout)) { $this->connectiontimeout = (int)$CFG->session_redis_connection_timeout; } if (isset($CFG->session_redis_max_retries)) { $this->maxretries = (int)$CFG->session_redis_max_retries; } $this->clock = di::get(clock::class); } #[\Override] public function init(): bool { $connected = $this->connect_to_redis(); $result = session_set_save_handler($this); if (!$result) { throw new exception('redissessionhandlerproblem', 'error'); } return $connected; } /** * Initiates a new connection to a Redis instance or RedisCluster * * @return bool True, if the connection was successfully established * @throws RedisException If the connection to a Redis instance failed * @throws RedisClusterException If the connection to a RedisCluster failed * @throws exception If a session handler error occurred */ protected function connect_to_redis(): bool { if (!extension_loaded('redis')) { throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension is not loaded'); } if (empty($this->host)) { throw new exception( 'sessionhandlerproblem', 'error', '', null, '$CFG->session_redis_host must be specified in config.php', ); } $version = phpversion('Redis'); if (!$version || version_compare($version, self::REDIS_MIN_EXTENSION_VERSION) <= 0) { throw new exception( errorcode: 'sessionhandlerproblem', module: 'error', debuginfo: sprintf('redis extension version must be at least %s', self::REDIS_MIN_EXTENSION_VERSION), ); } $encrypt = (bool) ($this->sslopts ?? false); // Set Redis server(s). $trimmedservers = []; foreach ($this->host as $host) { $server = strtolower(trim($host)); if (!empty($server)) { if ($server[0] === '/' || str_starts_with($server, 'unix://')) { $port = 0; $trimmedservers[] = $server; } else { $port = $this->port ?? 6379; // No Unix socket so set default port. if (strpos($server, ':')) { // Check for custom port. list($server, $port) = explode(':', $server); } $trimmedservers[] = $server.':'.$port; } // We only need the first record for the single redis. if (!$this->clustermode) { // Handle the case when the server is not a Unix domain socket. if ($port !== 0) { list($server, ) = explode(':', $trimmedservers[0]); } else { $server = $trimmedservers[0]; } break; } } } // TLS/SSL Configuration. $opts = []; if ($encrypt) { if ($this->clustermode) { $opts = $this->sslopts; } else { // For a single (non-cluster) Redis, the TLS/SSL config must be added to the 'stream' key. $opts['stream'] = $this->sslopts; } } // Add retries for connections to make sure it goes through. $counter = 1; $exceptionclass = $this->clustermode ? 'RedisClusterException' : 'RedisException'; while ($counter <= $this->maxretries) { $this->connection = null; // Make a connection to Redis server(s). try { // Create a $redis object of a RedisCluster or Redis class. $phpredisversion = phpversion('redis'); if ($this->clustermode) { if (version_compare($phpredisversion, '6.0.0', '>=')) { // Named parameters are fully supported starting from version 6.0.0. $this->connection = new \RedisCluster( name: null, seeds: $trimmedservers, timeout: $this->connectiontimeout, // Timeout. read_timeout: $this->connectiontimeout, // Read timeout. persistent: true, auth: $this->auth, context: !empty($opts) ? $opts : null, ); } else { $this->connection = new \RedisCluster( null, $trimmedservers, $this->connectiontimeout, $this->connectiontimeout, true, $this->auth, !empty($opts) ? $opts : null ); } } else { $delay = rand(100, 500); $this->connection = new \Redis(); if (version_compare($phpredisversion, '6.0.0', '>=')) { // Named parameters are fully supported starting from version 6.0.0. $this->connection->connect( host: $server, port: $port, timeout: $this->connectiontimeout, // Timeout. retry_interval: $delay, read_timeout: $this->connectiontimeout, // Read timeout. context: $opts, ); } else { $this->connection->connect( $server, $port, $this->connectiontimeout, null, $delay, $this->connectiontimeout, $opts ); } if ($this->auth !== '' && !$this->connection->auth($this->auth)) { throw new $exceptionclass('Unable to authenticate.'); } } if (!$this->connection->setOption(\Redis::OPT_SERIALIZER, $this->serializer)) { throw new $exceptionclass('Unable to set the Redis PHP Serializer option.'); } if ($this->prefix !== '') { // Use custom prefix on sessions. if (!$this->connection->setOption(\Redis::OPT_PREFIX, $this->prefix)) { throw new $exceptionclass('Unable to set the Redis Prefix option.'); } } // Check the server version. // The session handler requires a version of Redis server with support for SET command options (at least 2.6.12). // Note: In the case of a TLS connection, the connection will hang if the phpredis client does not communicate // with the server immediately after connect(). See https://github.com/phpredis/phpredis/issues/2332. // This version check satisfies that requirement. try { $serverversion = $this->connection->info('server')['redis_version']; } catch (RedisException | RedisClusterException $e) { // Some proxies e.g envoy or twemproxy lack support of INFO command. So just assume we meet the minimum // version requirement. $serverversion = self::REDIS_MIN_SERVER_VERSION; } if (version_compare($serverversion, self::REDIS_MIN_SERVER_VERSION) < 0) { throw new $exceptionclass(sprintf( "Version %s is not supported. The minimum version required is %s.", $serverversion, self::REDIS_MIN_SERVER_VERSION, )); } if ($this->database !== 0) { if (!$this->connection->select($this->database)) { throw new $exceptionclass('Unable to select the Redis database ' . $this->database . '.'); } } return true; } catch (RedisException | RedisClusterException $e) { $redishost = $this->clustermode ? implode(',', $this->host) : $server . ':' . $port; $logstring = "Failed to connect (try {$counter} out of " . $this->maxretries . ") to Redis "; $logstring .= "at ". $redishost .", the error returned was: {$e->getMessage()}"; debugging($logstring); } $counter++; // Introduce a random sleep between 100ms and 500ms. usleep(rand(100000, 500000)); } if (isset($logstring)) { // We have exhausted our retries; it's time to give up. throw new $exceptionclass($logstring); } return false; } /** * Update our session search path to include session name when opened. * * @param string $path unused session save path. (ignored) * @param string $name Session name for this session. (ignored) * @return bool true always as we will succeed. */ public function open(string $path, string $name): bool { return true; } /** * Close the session completely. We also remove all locks we may have obtained that aren't expired. * * @return bool true on success. false on unable to unlock sessions. */ public function close(): bool { $this->lasthash = null; try { foreach ($this->locks as $id => $expirytime) { if ($expirytime > $this->clock->time()) { $this->unlock_session($id); } unset($this->locks[$id]); } } catch (RedisException | RedisClusterException $e) { error_log('Failed talking to redis: '.$e->getMessage()); return false; } return true; } /** * Read the session data from storage * * @param string $id The session id to read from storage. * @return string|false The session data for PHP to process or false. * * @throws RedisException when we are unable to talk to the Redis server. */ public function read(string $id): string|false { try { if ($this->requires_write_lock()) { $this->lock_session($this->sessionkeyprefix . $id); } $keys = $this->connection->hmget($this->sessionkeyprefix . $id, ['userid', 'sessdata']); $userid = $keys['userid']; $sessiondata = $this->uncompress($keys['sessdata']); if ($sessiondata === false) { if ($this->requires_write_lock()) { $this->unlock_session($this->sessionkeyprefix . $id); } $this->lasthash = sha1(''); return ''; } // Do not update expiry if non-login user (0). This would affect the first access timeout. if ($userid != 0) { $maxlifetime = $this->get_maxlifetime($userid); $this->connection->expire($this->sessionkeyprefix . $id, $maxlifetime); $this->connection->expire($this->userkeyprefix . $userid, $maxlifetime); } } catch (RedisException | RedisClusterException $e) { error_log('Failed talking to redis: '.$e->getMessage()); throw $e; } // Update last hash. if ($sessiondata === null) { // As of PHP 8.1 we can't pass null to base64_encode. $sessiondata = ''; } $this->lasthash = sha1(base64_encode($sessiondata)); return $sessiondata; } /** * Compresses session data. * * @param mixed $value * @return string */ private function compress($value): string { switch ($this->compressor) { case self::COMPRESSION_NONE: return $value; case self::COMPRESSION_GZIP: return gzencode($value); case self::COMPRESSION_ZSTD: return zstd_compress($value); default: debugging("Invalid compressor: {$this->compressor}"); return $value; } } /** * Uncompresses session data. * * @param string $value * @return mixed */ private function uncompress($value) { if ($value === false) { return false; } switch ($this->compressor) { case self::COMPRESSION_NONE: break; case self::COMPRESSION_GZIP: $value = gzdecode($value); break; case self::COMPRESSION_ZSTD: $value = zstd_uncompress($value); break; default: debugging("Invalid compressor: {$this->compressor}"); } return $value; } /** * Write the serialized session data to our session store. * * @param string $id session id to write. * @param string $data session data * @return bool true on write success, false on failure */ public function write(string $id, string $data): bool { $hash = sha1(base64_encode($data)); // If the content has not changed don't bother writing. if ($hash === $this->lasthash) { return true; } if (is_null($this->connection)) { // The session has already been closed, don't attempt another write. error_log('Tried to write session: '.$id.' before open or after close.'); return false; } // We do not do locking here because memcached doesn't. Also // PHP does open, read, destroy, write, close. When a session doesn't exist. // There can be race conditions on new sessions racing each other but we can // address that in the future. try { $data = $this->compress($data); $this->connection->hset($this->sessionkeyprefix . $id, 'sessdata', $data); $keys = $this->connection->hmget($this->sessionkeyprefix . $id, ['userid', 'timecreated', 'timemodified']); $userid = $keys['userid']; // Don't update expiry if still first access. if ($keys['timecreated'] != $keys['timemodified']) { $maxlifetime = $this->get_maxlifetime($userid); $this->connection->expire($this->sessionkeyprefix . $id, $maxlifetime); if ($userid != 0) { $this->connection->expire($this->userkeyprefix . $userid, $maxlifetime); } } } catch (RedisException | RedisClusterException $e) { error_log('Failed talking to redis: '.$e->getMessage()); return false; } return true; } #[\Override] public function get_session_by_sid(string $sid): \stdClass { $this->connect_to_redis_if_required(); $keys = ["id", "state", "sid", "userid", "sessdata", "timecreated", "timemodified", "firstip", "lastip"]; $sessiondata = $this->connection->hmget($this->sessionkeyprefix . $sid, $keys); return (object)$sessiondata; } #[\Override] public function add_session(int $userid): \stdClass { $timestamp = $this->clock->time(); $sid = session_id(); $maxlifetime = $this->get_maxlifetime($userid, true); $sessiondata = [ 'id' => $sid, 'state' => '0', 'sid' => $sid, 'userid' => $userid, 'sessdata' => null, 'timecreated' => $timestamp, 'timemodified' => $timestamp, 'firstip' => getremoteaddr(), 'lastip' => getremoteaddr(), ]; // Do not manage store mapping from user to sid for non-login user (0). if ($userid != 0) { $userhashkey = $this->userkeyprefix . $userid; $this->connection->hSet($userhashkey, $sid, $timestamp); $this->connection->expire($userhashkey, $maxlifetime); } $sessionhashkey = $this->sessionkeyprefix . $sid; $this->connection->hmSet($sessionhashkey, $sessiondata); $this->connection->expire($sessionhashkey, $maxlifetime); return (object)$sessiondata; } #[\Override] public function get_sessions_by_userid(int $userid): array { $this->connect_to_redis_if_required(); $userhashkey = $this->userkeyprefix . $userid; $sessions = $this->connection->hGetAll($userhashkey); $records = []; foreach (array_keys($sessions) as $session) { $item = $this->connection->hGetAll($this->sessionkeyprefix . $session); if (!empty($item)) { $records[] = (object) $item; } } return $records; } #[\Override] public function update_session(\stdClass $record): bool { if (!isset($record->sid) && isset($record->id)) { $record->sid = $record->id; } // If record does not have userid set, we need to get it from the session. if (!isset($record->userid)) { $session = $this->get_session_by_sid($record->sid); $record->userid = $session->userid; } $sessionhashkey = $this->sessionkeyprefix . $record->sid; $userhashkey = $this->userkeyprefix . $record->userid; $recordata = (array) $record; unset($recordata['sid']); $this->connection->hmSet($sessionhashkey, $recordata); // Update the expiry time. $maxlifetime = $this->get_maxlifetime($record->userid); $this->connection->expire($sessionhashkey, $maxlifetime); if ($record->userid != 0) { $this->connection->expire($userhashkey, $maxlifetime); } return true; } #[\Override] public function get_all_sessions(): \Iterator { $sessions = []; $iterator = null; while (false !== ($keys = $this->connection->scan($iterator, '*' . $this->sessionkeyprefix . '*'))) { foreach ($keys as $key) { $sessions[] = $key; } } return new \ArrayIterator($sessions); } #[\Override] public function destroy_all(): bool { $this->connect_to_redis_if_required(); $sessions = $this->get_all_sessions(); foreach ($sessions as $session) { // Remove the prefixes from the session id, as destroy expects the raw session id. if (str_starts_with($session, $this->prefix . $this->sessionkeyprefix)) { $session = substr($session, strlen($this->prefix . $this->sessionkeyprefix)); } $this->destroy($session); } return true; } #[\Override] public function destroy(string $id): bool { $this->connect_to_redis_if_required(); $this->lasthash = null; try { $sessionhashkey = $this->sessionkeyprefix . $id; $userid = $this->connection->hget($sessionhashkey, "userid"); $userhashkey = $this->userkeyprefix . $userid; $this->connection->hDel($userhashkey, $id); $this->connection->unlink($sessionhashkey); $this->unlock_session($id); } catch (RedisException | RedisClusterException $e) { error_log('Failed talking to redis: '.$e->getMessage()); return false; } return true; } // phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameUnderscore #[\Override] public function gc(int $max_lifetime = 0): int|false { return 0; } // phpcs:enable /** * Get session maximum lifetime in seconds. * * @param int|null $userid The user id to calculate the max lifetime for. * @param bool $firstbrowseraccess This indicates that this is calculating the expiry when the key is first added. * The first access made by the browser has a shorter timeout to reduce abandoned sessions. * @return float|int */ private function get_maxlifetime(?int $userid = null, bool $firstbrowseraccess = false): float|int { global $CFG; // Guest user. if ($userid == $CFG->siteguest) { return $CFG->sessiontimeout * 5; } // All other users. if ($userid == 0 && $firstbrowseraccess) { $maxlifetime = $this->firstaccesstimeout; } else { // As per MDL-56823 - The following configures the session lifetime in redis to allow some // wriggle room in the user noticing they've been booted off and // letting them log back in before they lose their session entirely. $updatefreq = empty($CFG->session_update_timemodified_frequency) ? 20 : $CFG->session_update_timemodified_frequency; $maxlifetime = (int) $CFG->sessiontimeout + $updatefreq + MINSECS; } return $maxlifetime; } /** * Connection will be null if these methods are called from cli or where NO_MOODLE_COOKIES is used. * We need to check for this and create a new connection if required. * * @return void * @throws exception|RedisException|RedisClusterException If connection to Redis failed */ private function connect_to_redis_if_required(): void { if (is_null($this->connection)) { $this->connect_to_redis(); } } /** * Unlock a session. * * @param string $id Session id to be unlocked. */ protected function unlock_session($id) { if (isset($this->locks[$id])) { $this->connection->unlink("{$id}.lock"); unset($this->locks[$id]); } } /** * Obtain a session lock so we are the only one using it at the moment. * * @param string $id The session id to lock. * @return bool true when session was locked, exception otherwise. * @throws exception When we are unable to obtain a session lock. */ protected function lock_session($id) { $lockkey = "{$id}.lock"; $haslock = isset($this->locks[$id]) && $this->clock->time() < $this->locks[$id]; if ($haslock) { return true; } $startlocktime = $this->clock->time(); // To be able to ensure sessions don't write out of order we must obtain an exclusive lock // on the session for the entire time it is open. If another AJAX call, or page is using // the session then we just wait until it finishes before we can open the session. // Store the current host, process id and the request URI so it's easy to track who has the lock. $hostname = gethostname(); if ($hostname === false) { $hostname = 'UNKNOWN HOST'; } $pid = getmypid(); if ($pid === false) { $pid = 'UNKNOWN'; } $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : 'unknown uri'; $whoami = "[pid {$pid}] {$hostname}:$uri"; $haswarned = false; // Have we logged a lock warning? while (!$haslock) { $haslock = $this->connection->set($lockkey, $whoami, ['nx', 'ex' => $this->lockexpire]); if ($haslock) { $this->locks[$id] = $this->clock->time() + $this->lockexpire; return true; } if (!empty($this->acquirewarn) && !$haswarned && $this->clock->time() > $startlocktime + $this->acquirewarn) { // This is a warning to better inform users. $whohaslock = $this->connection->get($lockkey); // phpcs:ignore error_log( "Warning: Cannot obtain session lock for sid: $id within $this->acquirewarn seconds but will keep trying. " . "It is likely another page ($whohaslock) has a long session lock, or the session lock was never released.", ); $haswarned = true; } if ($this->clock->time() > $startlocktime + $this->acquiretimeout) { // This is a fatal error, better inform users. // It should not happen very often - all pages that need long time to execute // should close session immediately after access control checks. $whohaslock = $this->connection->get($lockkey); // phpcs:ignore error_log( "Error: Cannot obtain session lock for sid: $id within $this->acquiretimeout seconds. " . "It is likely another page ($whohaslock) has a long session lock, or the session lock was never released.", ); $acquiretimeout = format_time($this->acquiretimeout); $lockexpire = format_time($this->lockexpire); $a = (object)[ 'id' => substr($id, 0, 10), 'acquiretimeout' => $acquiretimeout, 'whohaslock' => $whohaslock, 'lockexpire' => $lockexpire, ]; throw new exception("sessioncannotobtainlock", 'error', '', $a); } 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($this->lockretry, (int)($this->lockretry * 1.1)); } 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); } throw new coding_exception('Unable to lock session'); } #[\Override] public function session_exists($sid) { if (!$this->connection) { return false; } try { $sessionhashkey = $this->sessionkeyprefix . $sid; return !empty($this->connection->exists($sessionhashkey)); } catch (RedisException | RedisClusterException $e) { return false; } } } PK"\ɀutility/cookie_helper.phpnu[. namespace core\session\utility; /** * Helper class providing utils dealing with cookies, particularly 3rd party cookies. * * This class primarily provides a means to augment outbound cookie headers, in order to satisfy browser-specific * requirements for setting 3rd party cookies. * * @package core * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cookie_helper { /** * Make sure the given attributes are set on the Set-Cookie response header identified by name=$cookiename. * * This function only affects Set-Cookie headers and modifies the headers directly with the required changes, if any. * * @param string $cookiename the cookie name. * @param array $attributes the attributes to set/ensure are set. * @return void */ public static function add_attributes_to_cookie_response_header(string $cookiename, array $attributes): void { $setcookieheaders = array_filter(headers_list(), function($val) { return preg_match("/Set-Cookie:/i", $val); }); if (empty($setcookieheaders)) { return; } $updatedheaders = self::cookie_response_headers_add_attributes($setcookieheaders, [$cookiename], $attributes); // Note: The header_remove() method is quite crude and removes all headers of that header name. header_remove('Set-Cookie'); foreach ($updatedheaders as $header) { header($header, false); } } /** * Given a list of HTTP header strings, return a list of HTTP header strings where the matched 'Set-Cookie' headers * have been updated with the attributes defined in $attribute - an array of strings. * * This method does not verify whether a given attribute is valid or not. It blindly sets it and returns the header * strings. It's up to calling code to determine whether an attribute makes sense or not. * * @param array $headerstrings the array of header strings. * @param array $cookiestomatch the array of cookie names to match. * @param array $attributes the attributes to set on each matched 'Set-Cookie' header. * @param bool $casesensitive whether to match the attribute in a case-sensitive way. * @return array the updated array of header strings. */ public static function cookie_response_headers_add_attributes(array $headerstrings, array $cookiestomatch, array $attributes, bool $casesensitive = false): array { return array_map(function($headerstring) use ($attributes, $casesensitive, $cookiestomatch) { if (!self::cookie_response_header_matches_names($headerstring, $cookiestomatch)) { return $headerstring; } foreach ($attributes as $attribute) { if (!self::cookie_response_header_contains_attribute($headerstring, $attribute, $casesensitive)) { $headerstring = self::cookie_response_header_append_attribute($headerstring, $attribute); } } return $headerstring; }, $headerstrings); } /** * Forces the expiry of the MoodleSession cookie. * * This is useful to force a new Set-Cookie header on the next redirect. * * @return void */ public static function expire_moodlesession(): void { global $CFG; $setcookieheader = array_filter(headers_list(), function($val) use ($CFG) { return self::cookie_response_header_matches_name($val, 'MoodleSession'.$CFG->sessioncookie); }); if (!empty($setcookieheader)) { $expirestr = 'Expires='.gmdate(DATE_RFC7231, time() - 60); self::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, [$expirestr]); } else { setcookie('MoodleSession'.$CFG->sessioncookie, '', time() - 60); } } /** * Check whether the header string is a 'Set-Cookie' header for the cookie identified by $cookiename. * * @param string $headerstring the header string to check. * @param string $cookiename the name of the cookie to match. * @return bool true if the header string is a Set-Cookie header for the named cookie, false otherwise. */ private static function cookie_response_header_matches_name(string $headerstring, string $cookiename): bool { // Generally match the format, but in a case-insensitive way so that 'set-cookie' and "SET-COOKIE" are both valid. return preg_match("/Set-Cookie: *$cookiename=/i", $headerstring) // Case-sensitive match on cookiename, which is case-sensitive. && preg_match("/: *$cookiename=/", $headerstring); } /** * Check whether the header string is a 'Set-Cookie' header for the cookies identified in the $cookienames array. * * @param string $headerstring the header string to check. * @param array $cookienames the array of cookie names to match. * @return bool true if the header string is a Set-Cookie header for one of the named cookies, false otherwise. */ private static function cookie_response_header_matches_names(string $headerstring, array $cookienames): bool { foreach ($cookienames as $cookiename) { if (self::cookie_response_header_matches_name($headerstring, $cookiename)) { return true; } } return false; } /** * Check whether the header string contains the given attribute. * * @param string $headerstring the header string to check. * @param string $attribute the attribute to check for. * @param bool $casesensitive whether to perform a case-sensitive check. * @return bool true if the header contains the attribute, false otherwise. */ private static function cookie_response_header_contains_attribute(string $headerstring, string $attribute, bool $casesensitive): bool { if ($casesensitive) { return str_contains($headerstring, $attribute); } return str_contains(strtolower($headerstring), strtolower($attribute)); } /** * Append the given attribute to the header string. * * @param string $headerstring the header string to append to. * @param string $attribute the attribute to append. * @return string the updated header string. */ private static function cookie_response_header_append_attribute(string $headerstring, string $attribute): string { $headerstring = rtrim($headerstring, ';'); // Sometimes included. return "$headerstring; $attribute;"; } } PK"\PX X mock_handler.phpnu[. namespace core\tests\session; use core\clock; use core\di; use core\session\database; /** * Mock handler methods class. * * @package core * @author Darren Cocco * @author Trisha Milan * @copyright 2022 Monash University (http://www.monash.edu) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mock_handler extends database { #[\Override] public function init(): bool { // Nothing special to do in the mock. return true; } #[\Override] public function session_exists($sid): bool { global $DB; return $DB->record_exists('sessions', ['sid' => $sid]); } /** * Insert a new session record to be used in unit tests. * * @param \stdClass $record * @return int Inserted record id. */ public function add_test_session(\stdClass $record): int { global $DB, $USER; $data = new \stdClass(); $data->state = $record->state ?? 0; $data->sid = $record->sid ?? session_id(); $data->sessdata = $record->sessdata ?? null; $data->userid = $record->userid ?? $USER->id; $data->timecreated = $record->timecreated ?? di::get(clock::class)->time(); $data->timemodified = $record->timemodified ?? di::get(clock::class)->time(); $data->firstip = $record->firstip ?? getremoteaddr(); $data->lastip = $record->lastip ?? getremoteaddr(); return $DB->insert_record('sessions', $data); } #[\Override] public function get_all_sessions(): \Iterator { global $DB; $records = $DB->get_records('sessions'); return new \ArrayIterator($records); } /** * Returns the number of all sessions stored. * * @return int */ public function count_sessions(): int { global $DB; return $DB->count_records('sessions'); } } PK\$vhhlang/en/cachestore_session.phpnu[PK\f2/~G~Glib.phpnu[PK\` kMversion.phpnu[PK\ta>''Rclasses/privacy/provider.phpnu[PK\,a4hCCbtests/store_test.phpnu[PK"\Dx(( memcached.phpnu[PK"\زloginas_helper.phpnu[PK"\`aZ ѹexternal.phpnu[PK"\J database.phpnu[PK"\H! exception.phpnu[PK"\MOf0 0 +file.phpnu[PK"\Q manager.phpnu[PK"\(E'E' handler.phpnu[PK"\aq Nredis.phpnu[PK"\ɀvutility/cookie_helper.phpnu[PK"\PX X ңmock_handler.phpnu[PKj