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 /** 18 * Database based session handler. 19 * 20 * @package core 21 * @copyright 2013 Petr Skoda {@link http://skodak.org} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core\session; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Database based session handler. 31 * 32 * @package core 33 * @copyright 2013 Petr Skoda {@link http://skodak.org} 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class database extends handler { 37 /** @var \stdClass $record session record */ 38 protected $recordid = null; 39 40 /** @var \moodle_database $database session database */ 41 protected $database = null; 42 43 /** @var bool $failed session read/init failed, do not write back to DB */ 44 protected $failed = false; 45 46 /** @var string $lasthash hash of the session data content */ 47 protected $lasthash = null; 48 49 /** @var int $acquiretimeout how long to wait for session lock */ 50 protected $acquiretimeout = 120; 51 52 /** 53 * Create new instance of handler. 54 */ 55 public function __construct() { 56 global $DB, $CFG; 57 // Note: we store the reference here because we need to modify database in shutdown handler. 58 $this->database = $DB; 59 60 if (!empty($CFG->session_database_acquire_lock_timeout)) { 61 $this->acquiretimeout = (int)$CFG->session_database_acquire_lock_timeout; 62 } 63 } 64 65 /** 66 * Init session handler. 67 */ 68 public function init() { 69 if (!$this->database->session_lock_supported()) { 70 throw new exception('sessionhandlerproblem', 'error', '', null, 'Database does not support session locking'); 71 } 72 73 $result = session_set_save_handler(array($this, 'handler_open'), 74 array($this, 'handler_close'), 75 array($this, 'handler_read'), 76 array($this, 'handler_write'), 77 array($this, 'handler_destroy'), 78 array($this, 'handler_gc')); 79 if (!$result) { 80 throw new exception('dbsessionhandlerproblem', 'error'); 81 } 82 } 83 84 /** 85 * Check the backend contains data for this session id. 86 * 87 * Note: this is intended to be called from manager::session_exists() only. 88 * 89 * @param string $sid 90 * @return bool true if session found. 91 */ 92 public function session_exists($sid) { 93 // It was already checked in the calling code that the record in sessions table exists. 94 return true; 95 } 96 97 /** 98 * Kill all active sessions, the core sessions table is 99 * purged afterwards. 100 */ 101 public function kill_all_sessions() { 102 // Nothing to do, the sessions table is cleared from core. 103 return; 104 } 105 106 /** 107 * Kill one session, the session record is removed afterwards. 108 * @param string $sid 109 */ 110 public function kill_session($sid) { 111 // Nothing to do, the sessions table is purged afterwards. 112 return; 113 } 114 115 /** 116 * Open session handler. 117 * 118 * {@see http://php.net/manual/en/function.session-set-save-handler.php} 119 * 120 * @param string $save_path 121 * @param string $session_name 122 * @return bool success 123 */ 124 public function handler_open($save_path, $session_name) { 125 // Note: we use the already open database. 126 return true; 127 } 128 129 /** 130 * Close session handler. 131 * 132 * {@see http://php.net/manual/en/function.session-set-save-handler.php} 133 * 134 * @return bool success 135 */ 136 public function handler_close() { 137 if ($this->recordid) { 138 try { 139 $this->database->release_session_lock($this->recordid); 140 } catch (\Exception $ex) { 141 // Ignore any problems. 142 } 143 } 144 $this->recordid = null; 145 $this->lasthash = null; 146 return true; 147 } 148 149 /** 150 * Read session handler. 151 * 152 * {@see http://php.net/manual/en/function.session-set-save-handler.php} 153 * 154 * @param string $sid 155 * @return string 156 */ 157 public function handler_read($sid) { 158 try { 159 if (!$record = $this->database->get_record('sessions', array('sid'=>$sid), 'id')) { 160 // Let's cheat and skip locking if this is the first access, 161 // do not create the record here, let the manager do it after session init. 162 $this->failed = false; 163 $this->recordid = null; 164 $this->lasthash = sha1(''); 165 return ''; 166 } 167 if ($this->recordid and $this->recordid != $record->id) { 168 error_log('Second session read with different record id detected, cannot read session'); 169 $this->failed = true; 170 $this->recordid = null; 171 return ''; 172 } 173 if (!$this->recordid) { 174 // Lock session if exists and not already locked. 175 if ($this->requires_write_lock()) { 176 $this->database->get_session_lock($record->id, $this->acquiretimeout); 177 } 178 $this->recordid = $record->id; 179 } 180 } catch (\dml_sessionwait_exception $ex) { 181 // This is a fatal error, better inform users. 182 // It should not happen very often - all pages that need long time to execute 183 // should close session immediately after access control checks. 184 error_log('Cannot obtain session lock for sid: '.$sid); 185 $this->failed = true; 186 throw $ex; 187 188 } catch (\Exception $ex) { 189 // Do not rethrow exceptions here, this should not happen. 190 error_log('Unknown exception when starting database session : '.$sid.' - '.$ex->getMessage()); 191 $this->failed = true; 192 $this->recordid = null; 193 return ''; 194 } 195 196 // Finally read the full session data because we know we have the lock now. 197 if (!$record = $this->database->get_record('sessions', array('id'=>$record->id), 'id, sessdata')) { 198 // Ignore - something else just deleted the session record. 199 $this->failed = true; 200 $this->recordid = null; 201 return ''; 202 } 203 $this->failed = false; 204 205 if (is_null($record->sessdata)) { 206 $data = ''; 207 $this->lasthash = sha1(''); 208 } else { 209 $data = base64_decode($record->sessdata); 210 $this->lasthash = sha1($record->sessdata); 211 } 212 213 return $data; 214 } 215 216 /** 217 * Write session handler. 218 * 219 * {@see http://php.net/manual/en/function.session-set-save-handler.php} 220 * 221 * NOTE: Do not write to output or throw any exceptions! 222 * Hopefully the next page is going to display nice error or it recovers... 223 * 224 * @param string $sid 225 * @param string $session_data 226 * @return bool success 227 */ 228 public function handler_write($sid, $session_data) { 229 if ($this->failed) { 230 // Do not write anything back - we failed to start the session properly. 231 return false; 232 } 233 234 $sessdata = base64_encode($session_data); // There might be some binary mess :-( 235 $hash = sha1($sessdata); 236 237 if ($hash === $this->lasthash) { 238 return true; 239 } 240 241 try { 242 if ($this->recordid) { 243 $this->database->set_field('sessions', 'sessdata', $sessdata, array('id'=>$this->recordid)); 244 } else { 245 // This happens in the first request when session record was just created in manager. 246 $this->database->set_field('sessions', 'sessdata', $sessdata, array('sid'=>$sid)); 247 } 248 } catch (\Exception $ex) { 249 // Do not rethrow exceptions here, this should not happen. 250 error_log('Unknown exception when writing database session data : '.$sid.' - '.$ex->getMessage()); 251 } 252 253 return true; 254 } 255 256 /** 257 * Destroy session handler. 258 * 259 * {@see http://php.net/manual/en/function.session-set-save-handler.php} 260 * 261 * @param string $sid 262 * @return bool success 263 */ 264 public function handler_destroy($sid) { 265 if (!$session = $this->database->get_record('sessions', array('sid'=>$sid), 'id, sid')) { 266 if ($sid == session_id()) { 267 $this->recordid = null; 268 $this->lasthash = null; 269 } 270 return true; 271 } 272 273 if ($this->recordid and $session->id == $this->recordid) { 274 try { 275 $this->database->release_session_lock($this->recordid); 276 } catch (\Exception $ex) { 277 // Ignore problems. 278 } 279 $this->recordid = null; 280 $this->lasthash = null; 281 } 282 283 $this->database->delete_records('sessions', array('id'=>$session->id)); 284 285 return true; 286 } 287 288 /** 289 * GC session handler. 290 * 291 * {@see http://php.net/manual/en/function.session-set-save-handler.php} 292 * 293 * @param int $ignored_maxlifetime moodle uses special timeout rules 294 * @return bool success 295 */ 296 public function handler_gc($ignored_maxlifetime) { 297 // This should do something only if cron is not running properly... 298 if (!$stalelifetime = ini_get('session.gc_maxlifetime')) { 299 return true; 300 } 301 $params = array('purgebefore' => (time() - $stalelifetime)); 302 $this->database->delete_records_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params); 303 return true; 304 } 305 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body