| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 12 | // GNU General Public License for more details.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 14 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 15 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | namespace core\session;
 | 
        
           |  |  | 18 |   | 
        
           |  |  | 19 | /**
 | 
        
           |  |  | 20 |  * Memcached based session handler.
 | 
        
           |  |  | 21 |  *
 | 
        
           |  |  | 22 |  * @package    core
 | 
        
           |  |  | 23 |  * @copyright  2013 Petr Skoda {@link http://skodak.org}
 | 
        
           |  |  | 24 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 25 |  */
 | 
        
           |  |  | 26 | class memcached extends handler {
 | 
        
           |  |  | 27 |     /** @var string $savepath save_path string  */
 | 
        
           |  |  | 28 |     protected $savepath;
 | 
        
           |  |  | 29 |   | 
        
           |  |  | 30 |     /** @var array $servers list of servers parsed from save_path */
 | 
        
           |  |  | 31 |     protected $servers;
 | 
        
           |  |  | 32 |   | 
        
           |  |  | 33 |     /** @var string $prefix session key prefix  */
 | 
        
           |  |  | 34 |     protected $prefix;
 | 
        
           |  |  | 35 |   | 
        
           |  |  | 36 |     /** @var int $acquiretimeout how long to wait for session lock */
 | 
        
           |  |  | 37 |     protected $acquiretimeout = 120;
 | 
        
           |  |  | 38 |   | 
        
           |  |  | 39 |     /**
 | 
        
           |  |  | 40 |      * @var int $lockexpire how long to wait before expiring the lock so that other requests
 | 
        
           |  |  | 41 |      * may continue execution, ignored if PECL memcached is below version 2.2.0.
 | 
        
           |  |  | 42 |      */
 | 
        
           |  |  | 43 |     protected $lockexpire = 7200;
 | 
        
           |  |  | 44 |   | 
        
           |  |  | 45 |     /**
 | 
        
           |  |  | 46 |      * @var integer $lockretrysleep Used for memcached 3.x (PHP7), the amount of time to
 | 
        
           |  |  | 47 |      * sleep between attempts to acquire the session lock. Mimics the deprecated config
 | 
        
           |  |  | 48 |      * memcached.sess_lock_wait.
 | 
        
           |  |  | 49 |      */
 | 
        
           |  |  | 50 |     protected $lockretrysleep = 150;
 | 
        
           |  |  | 51 |   | 
        
           |  |  | 52 |     /**
 | 
        
           |  |  | 53 |      * Create new instance of handler.
 | 
        
           |  |  | 54 |      */
 | 
        
           |  |  | 55 |     public function __construct() {
 | 
        
           |  |  | 56 |         global $CFG;
 | 
        
           |  |  | 57 |   | 
        
           |  |  | 58 |         if (empty($CFG->session_memcached_save_path)) {
 | 
        
           |  |  | 59 |             $this->savepath = '';
 | 
        
           |  |  | 60 |         } else {
 | 
        
           |  |  | 61 |             $this->savepath =  $CFG->session_memcached_save_path;
 | 
        
           |  |  | 62 |         }
 | 
        
           |  |  | 63 |   | 
        
           |  |  | 64 |         if (empty($this->savepath)) {
 | 
        
           |  |  | 65 |             $this->servers = array();
 | 
        
           |  |  | 66 |         } else {
 | 
        
           |  |  | 67 |             $this->servers = self::connection_string_to_memcache_servers($this->savepath);
 | 
        
           |  |  | 68 |         }
 | 
        
           |  |  | 69 |   | 
        
           |  |  | 70 |         if (empty($CFG->session_memcached_prefix)) {
 | 
        
           |  |  | 71 |             $this->prefix = ini_get('memcached.sess_prefix');
 | 
        
           |  |  | 72 |         } else {
 | 
        
           |  |  | 73 |             $this->prefix = $CFG->session_memcached_prefix;
 | 
        
           |  |  | 74 |         }
 | 
        
           |  |  | 75 |   | 
        
           |  |  | 76 |         if (!empty($CFG->session_memcached_acquire_lock_timeout)) {
 | 
        
           |  |  | 77 |             $this->acquiretimeout = (int)$CFG->session_memcached_acquire_lock_timeout;
 | 
        
           |  |  | 78 |         }
 | 
        
           |  |  | 79 |   | 
        
           |  |  | 80 |         if (!empty($CFG->session_memcached_lock_expire)) {
 | 
        
           |  |  | 81 |             $this->lockexpire = (int)$CFG->session_memcached_lock_expire;
 | 
        
           |  |  | 82 |         }
 | 
        
           |  |  | 83 |   | 
        
           |  |  | 84 |         if (!empty($CFG->session_memcached_lock_retry_sleep)) {
 | 
        
           |  |  | 85 |             $this->lockretrysleep = (int)$CFG->session_memcached_lock_retry_sleep;
 | 
        
           |  |  | 86 |         }
 | 
        
           |  |  | 87 |     }
 | 
        
           |  |  | 88 |   | 
        
           | 1441 | ariadna | 89 |     #[\Override]
 | 
        
           | 1 | efrain | 90 |     public function start() {
 | 
        
           |  |  | 91 |         ini_set('memcached.sess_locking', $this->requires_write_lock() ? '1' : '0');
 | 
        
           |  |  | 92 |   | 
        
           |  |  | 93 |         // NOTE: memcached before 2.2.0 expires session locks automatically after max_execution_time,
 | 
        
           |  |  | 94 |         //       this leads to major difference compared to other session drivers that timeout
 | 
        
           |  |  | 95 |         //       and stop the second request execution instead.
 | 
        
           |  |  | 96 |   | 
        
           |  |  | 97 |         $default = ini_get('max_execution_time');
 | 
        
           |  |  | 98 |         set_time_limit($this->acquiretimeout);
 | 
        
           |  |  | 99 |   | 
        
           |  |  | 100 |         $isnewsession = empty($_COOKIE[session_name()]);
 | 
        
           |  |  | 101 |         $starttimer = microtime(true);
 | 
        
           |  |  | 102 |   | 
        
           |  |  | 103 |         $result = parent::start();
 | 
        
           |  |  | 104 |   | 
        
           |  |  | 105 |         // If session_start returned TRUE, but it took as long
 | 
        
           |  |  | 106 |         // as the timeout value, and the $_SESSION returned is
 | 
        
           |  |  | 107 |         // empty when should not have been (isnewsession false)
 | 
        
           |  |  | 108 |         // then assume it did timeout and is invalid.
 | 
        
           |  |  | 109 |         // Add 1 second to elapsed time to account for inexact
 | 
        
           |  |  | 110 |         // timings in php_memcached_session.c.
 | 
        
           |  |  | 111 |         // @TODO Remove this check when php-memcached is fixed
 | 
        
           |  |  | 112 |         // to return false after key lock acquisition timeout.
 | 
        
           |  |  | 113 |         if (!$isnewsession && $result && count($_SESSION) == 0
 | 
        
           |  |  | 114 |             && (microtime(true) - $starttimer + 1) >= floatval($this->acquiretimeout)) {
 | 
        
           |  |  | 115 |             $result = false;
 | 
        
           |  |  | 116 |         }
 | 
        
           |  |  | 117 |   | 
        
           |  |  | 118 |         set_time_limit($default);
 | 
        
           |  |  | 119 |         return $result;
 | 
        
           |  |  | 120 |     }
 | 
        
           |  |  | 121 |   | 
        
           | 1441 | ariadna | 122 |     #[\Override]
 | 
        
           | 1 | efrain | 123 |     public function init() {
 | 
        
           |  |  | 124 |         if (!extension_loaded('memcached')) {
 | 
        
           |  |  | 125 |             throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension is not loaded');
 | 
        
           |  |  | 126 |         }
 | 
        
           |  |  | 127 |         $version = phpversion('memcached');
 | 
        
           |  |  | 128 |         if (!$version or version_compare($version, '2.0') < 0) {
 | 
        
           |  |  | 129 |             throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension version must be at least 2.0');
 | 
        
           |  |  | 130 |         }
 | 
        
           |  |  | 131 |         if (empty($this->savepath)) {
 | 
        
           |  |  | 132 |             throw new exception('sessionhandlerproblem', 'error', '', null, '$CFG->session_memcached_save_path must be specified in config.php');
 | 
        
           |  |  | 133 |         }
 | 
        
           |  |  | 134 |   | 
        
           |  |  | 135 |         ini_set('session.save_handler', 'memcached');
 | 
        
           |  |  | 136 |         ini_set('session.save_path', $this->savepath);
 | 
        
           |  |  | 137 |         ini_set('memcached.sess_prefix', $this->prefix);
 | 
        
           |  |  | 138 |         ini_set('memcached.sess_lock_expire', $this->lockexpire);
 | 
        
           |  |  | 139 |   | 
        
           |  |  | 140 |         if (version_compare($version, '3.0.0-dev') >= 0) {
 | 
        
           |  |  | 141 |             // With memcached 3.x (PHP 7) we configure the max retries to make and the time to sleep between each retry.
 | 
        
           |  |  | 142 |             // There are two sleep config values, an initial and a max value.
 | 
        
           |  |  | 143 |             // After each attempt the memcached module adjusts the sleep value to be the lesser of the configured max
 | 
        
           |  |  | 144 |             // value, or 2X the previous value.
 | 
        
           |  |  | 145 |             // With default memcached.ini configs (5, 1s, 2s) the result is only 5 attempts to lock over 9 sec.
 | 
        
           |  |  | 146 |             // To mimic the behavior of the 2.2.x module so we get more attempts and much more frequently, config both
 | 
        
           |  |  | 147 |             // sleep values to the old default value of 150 msec (making it constant) and calculate number of retries
 | 
        
           |  |  | 148 |             // using the existing Moodle config $CFG->session_memcached_acquire_lock_timeout.
 | 
        
           |  |  | 149 |             // Doing this so admins configure session lock attempt timeout in familiar terms, and more straight-forward
 | 
        
           |  |  | 150 |             // to detect if lock attempt timeout has occurred in start().
 | 
        
           |  |  | 151 |             // If _min and _max values are not equal, the actual lock acquire timeout will not be the expected
 | 
        
           |  |  | 152 |             // configured value in $CFG->session_memcached_acquire_lock_timeout; this will cause session data loss when
 | 
        
           |  |  | 153 |             // failure to acquire the lock is not detected.
 | 
        
           |  |  | 154 |             ini_set('memcached.sess_lock_wait_min', $this->lockretrysleep);
 | 
        
           |  |  | 155 |             ini_set('memcached.sess_lock_wait_max', $this->lockretrysleep);
 | 
        
           |  |  | 156 |             ini_set('memcached.sess_lock_retries', (int)(($this->acquiretimeout * 1000) / $this->lockretrysleep) + 1);
 | 
        
           |  |  | 157 |         } else {
 | 
        
           |  |  | 158 |             // With memcached 2.2.x we configure max time to attempt lock, and accept default value (in memcached.ini)
 | 
        
           |  |  | 159 |             // for sleep time between each attempt (usually 150 msec), then memcached calculates the max number of
 | 
        
           |  |  | 160 |             // retries to make.
 | 
        
           |  |  | 161 |             ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
 | 
        
           |  |  | 162 |         }
 | 
        
           |  |  | 163 |   | 
        
           |  |  | 164 |     }
 | 
        
           |  |  | 165 |   | 
        
           | 1441 | ariadna | 166 |     #[\Override]
 | 
        
           | 1 | efrain | 167 |     public function session_exists($sid) {
 | 
        
           |  |  | 168 |         if (!$this->servers) {
 | 
        
           |  |  | 169 |             return false;
 | 
        
           |  |  | 170 |         }
 | 
        
           |  |  | 171 |   | 
        
           |  |  | 172 |         // Go through the list of all servers because
 | 
        
           |  |  | 173 |         // we do not know where the session handler put the
 | 
        
           |  |  | 174 |         // data.
 | 
        
           |  |  | 175 |   | 
        
           |  |  | 176 |         foreach ($this->servers as $server) {
 | 
        
           |  |  | 177 |             list($host, $port) = $server;
 | 
        
           |  |  | 178 |             $memcached = new \Memcached();
 | 
        
           |  |  | 179 |             $memcached->addServer($host, $port);
 | 
        
           |  |  | 180 |             $value = $memcached->get($this->prefix . $sid);
 | 
        
           |  |  | 181 |             $memcached->quit();
 | 
        
           |  |  | 182 |             if ($value !== false) {
 | 
        
           |  |  | 183 |                 return true;
 | 
        
           |  |  | 184 |             }
 | 
        
           |  |  | 185 |         }
 | 
        
           |  |  | 186 |   | 
        
           |  |  | 187 |         return false;
 | 
        
           |  |  | 188 |     }
 | 
        
           |  |  | 189 |   | 
        
           | 1441 | ariadna | 190 |     #[\Override]
 | 
        
           |  |  | 191 |     public function destroy_all(): bool {
 | 
        
           | 1 | efrain | 192 |         global $DB;
 | 
        
           |  |  | 193 |         if (!$this->servers) {
 | 
        
           | 1441 | ariadna | 194 |             return false;
 | 
        
           | 1 | efrain | 195 |         }
 | 
        
           |  |  | 196 |   | 
        
           |  |  | 197 |         // Go through the list of all servers because
 | 
        
           |  |  | 198 |         // we do not know where the session handler put the
 | 
        
           |  |  | 199 |         // data.
 | 
        
           |  |  | 200 |   | 
        
           |  |  | 201 |         $memcacheds = array();
 | 
        
           |  |  | 202 |         foreach ($this->servers as $server) {
 | 
        
           |  |  | 203 |             list($host, $port) = $server;
 | 
        
           |  |  | 204 |             $memcached = new \Memcached();
 | 
        
           |  |  | 205 |             $memcached->addServer($host, $port);
 | 
        
           |  |  | 206 |             $memcacheds[] = $memcached;
 | 
        
           |  |  | 207 |         }
 | 
        
           |  |  | 208 |   | 
        
           |  |  | 209 |         // Note: this can be significantly improved by fetching keys from memcached,
 | 
        
           |  |  | 210 |         //       but we need to make sure we are not deleting somebody else's sessions.
 | 
        
           |  |  | 211 |   | 
        
           |  |  | 212 |         $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid');
 | 
        
           |  |  | 213 |         foreach ($rs as $record) {
 | 
        
           |  |  | 214 |             foreach ($memcacheds as $memcached) {
 | 
        
           |  |  | 215 |                 $memcached->delete($this->prefix . $record->sid);
 | 
        
           |  |  | 216 |             }
 | 
        
           |  |  | 217 |         }
 | 
        
           |  |  | 218 |         $rs->close();
 | 
        
           |  |  | 219 |   | 
        
           |  |  | 220 |         foreach ($memcacheds as $memcached) {
 | 
        
           |  |  | 221 |             $memcached->quit();
 | 
        
           |  |  | 222 |         }
 | 
        
           | 1441 | ariadna | 223 |   | 
        
           |  |  | 224 |         return parent::destroy_all();
 | 
        
           | 1 | efrain | 225 |     }
 | 
        
           |  |  | 226 |   | 
        
           | 1441 | ariadna | 227 |     #[\Override]
 | 
        
           |  |  | 228 |     public function destroy(string $id): bool {
 | 
        
           | 1 | efrain | 229 |         if (!$this->servers) {
 | 
        
           | 1441 | ariadna | 230 |             return false;
 | 
        
           | 1 | efrain | 231 |         }
 | 
        
           |  |  | 232 |   | 
        
           |  |  | 233 |         // Go through the list of all servers because
 | 
        
           |  |  | 234 |         // we do not know where the session handler put the
 | 
        
           |  |  | 235 |         // data.
 | 
        
           |  |  | 236 |   | 
        
           |  |  | 237 |         foreach ($this->servers as $server) {
 | 
        
           |  |  | 238 |             list($host, $port) = $server;
 | 
        
           |  |  | 239 |             $memcached = new \Memcached();
 | 
        
           |  |  | 240 |             $memcached->addServer($host, $port);
 | 
        
           | 1441 | ariadna | 241 |             $memcached->delete($this->prefix . $id);
 | 
        
           | 1 | efrain | 242 |             $memcached->quit();
 | 
        
           |  |  | 243 |         }
 | 
        
           | 1441 | ariadna | 244 |   | 
        
           |  |  | 245 |         return parent::destroy($id);
 | 
        
           | 1 | efrain | 246 |     }
 | 
        
           |  |  | 247 |   | 
        
           |  |  | 248 |     /**
 | 
        
           |  |  | 249 |      * Convert a connection string to an array of servers.
 | 
        
           |  |  | 250 |      *
 | 
        
           |  |  | 251 |      * "abc:123, xyz:789" to
 | 
        
           |  |  | 252 |      *  [
 | 
        
           |  |  | 253 |      *      ['abc', '123'],
 | 
        
           |  |  | 254 |      *      ['xyz', '789'],
 | 
        
           |  |  | 255 |      *  ]
 | 
        
           |  |  | 256 |      *
 | 
        
           |  |  | 257 |      * @param   string  $str save_path value containing memcached connection string
 | 
        
           |  |  | 258 |      * @return  array[]
 | 
        
           |  |  | 259 |      */
 | 
        
           |  |  | 260 |     protected static function connection_string_to_memcache_servers(string $str): array {
 | 
        
           |  |  | 261 |         $servers = [];
 | 
        
           |  |  | 262 |         $parts   = explode(',', $str);
 | 
        
           |  |  | 263 |         foreach ($parts as $part) {
 | 
        
           |  |  | 264 |             $part = trim($part);
 | 
        
           |  |  | 265 |             $pos  = strrpos($part, ':');
 | 
        
           |  |  | 266 |             if ($pos !== false) {
 | 
        
           |  |  | 267 |                 $host = substr($part, 0, $pos);
 | 
        
           |  |  | 268 |                 $port = substr($part, ($pos + 1));
 | 
        
           |  |  | 269 |             } else {
 | 
        
           |  |  | 270 |                 $host = $part;
 | 
        
           |  |  | 271 |                 $port = 11211;
 | 
        
           |  |  | 272 |             }
 | 
        
           |  |  | 273 |             $servers[] = [$host, $port];
 | 
        
           |  |  | 274 |         }
 | 
        
           |  |  | 275 |         return $servers;
 | 
        
           |  |  | 276 |     }
 | 
        
           |  |  | 277 | }
 |