MDL-31501 rework user session architecture
authorPetr Škoda <commits@skodak.org>
Sun, 8 Sep 2013 06:38:52 +0000 (08:38 +0200)
committerPetr Škoda <commits@skodak.org>
Sat, 21 Sep 2013 11:11:56 +0000 (13:11 +0200)
List of changes:
 * New OOP API using PHP namespace \core\session\.
 * All handlers now update the sessions table consistently.
 * Experimental DB session support in Oracle.
 * Full support for session file handler (filesystem locking required).
 * New option for alternative session directory.
 * Official memcached session handler support.
 * Workaround for memcached version with non-functional gc.
 * Improved security - forced session id regeneration.
 * Improved compatibility with recent PHP releases.
 * Fixed borked CSS during install in debug mode.
 * Switched to file based sessions in new installs.
 * DB session setting disappears if DB does not support sessions.
 * DB session setting disappears if session handler specified in config.php.
 * Fast purging of sessions used in request only.
 * No legacy distinction -  file, database and memcached support the same functionality.
 * Session handler name included in performance info.
 * Fixed user_loggedin and user_loggedout event triggering.
 * Other minor bugfixing and improvements.
 * Fixed database session segfault if MUC disposed before $DB.

Limitations:
 * Session access time is now updated right after session start.
 * Support for $CFG->sessionlockloggedinonly was removed.
 * First request does not update userid in sessions table.
 * The timeouts may break badly if server hosting forces PHP.ini session settings.
 * The session GC is a lot slower, we do not rely on external session timeouts.
 * There cannot be any hooks triggered at the session write time.
 * File and memcached handlers do not support session lock acquire timeouts.
 * Some low level PHP session functions can not be used directly in Moodle code.

77 files changed:
admin/auth.php
admin/cli/upgrade.php
admin/cron.php
admin/index.php
admin/settings/server.php
admin/tool/assignmentupgrade/batchupgrade.php
admin/tool/dbtransfer/locallib.php
admin/tool/generator/cli/maketestcourse.php
admin/tool/generator/cli/maketestsite.php
admin/tool/uploaduser/index.php
admin/user.php
admin/user/user_bulk_delete.php
auth/ldap/auth.php
auth/mnet/auth.php
auth/shibboleth/index.php
badges/ajax.php
blocks/html/lib.php
blocks/mnet_hosts/block_mnet_hosts.php
calendar/tests/calendartype_test.php
config-dist.php
course/loginas.php
draftfile.php
enrol/index.php
file.php
lang/en/error.php
lib/authlib.php
lib/classes/event/user_loggedin.php
lib/classes/event/user_loggedout.php
lib/classes/session/database.php [new file with mode: 0644]
lib/classes/session/exception.php [new file with mode: 0644]
lib/classes/session/file.php [new file with mode: 0644]
lib/classes/session/handler.php [new file with mode: 0644]
lib/classes/session/manager.php [new file with mode: 0644]
lib/classes/session/memcached.php [new file with mode: 0644]
lib/cronlib.php
lib/datalib.php
lib/deprecatedlib.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlite3_pdo_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/filelib.php
lib/installlib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/util.php
lib/phpunit/tests/advanced_test.php
lib/sessionlib.php
lib/setup.php
lib/setuplib.php
lib/tests/authlib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/moodlelib_test.php
lib/tests/session_manager_test.php [new file with mode: 0644]
lib/tests/sessionlib_test.php [deleted file]
lib/upgrade.txt
lib/weblib.php
login/change_password.php
login/token.php
mod/assignment/type/online/assignment.class.php
mod/chat/chat_ajax.php
mod/chat/gui_header_js/insert.php
mod/quiz/report/overview/report.php
report/log/index.php
report/loglive/index.php
repository/lib.php
repository/tests/repositorylib_test.php
rss/file.php
theme/mymobile/renderers.php
user/editadvanced.php
user/index.php
webservice/lib.php

index 934acd3..300dead 100644 (file)
@@ -51,7 +51,7 @@ switch ($action) {
         if ($auth == $CFG->registerauth) {
             set_config('registerauth', '');
         }
-        session_gc(); // remove stale sessions
+        \core\session\manager::gc(); // Remove stale sessions.
         break;
 
     case 'enable':
@@ -61,7 +61,7 @@ switch ($action) {
             $authsenabled = array_unique($authsenabled);
             set_config('auth', implode(',', $authsenabled));
         }
-        session_gc(); // remove stale sessions
+        \core\session\manager::gc(); // Remove stale sessions.
         break;
 
     case 'down':
index 98e8726..28d7c8d 100644 (file)
@@ -172,7 +172,7 @@ set_config('branch', $branch);
 upgrade_noncore(true);
 
 // log in as admin - we need doanything permission when applying defaults
-session_set_user(get_admin());
+\core\session\manager::set_user(get_admin());
 
 // apply all default settings, just in case do it twice to fill all defaults
 admin_apply_default_settings(NULL, false);
index e411545..ed19d46 100644 (file)
@@ -53,7 +53,7 @@ require_once($CFG->libdir.'/clilib.php');
 require_once($CFG->libdir.'/cronlib.php');
 
 // extra safety
-session_get_instance()->write_close();
+\core\session\manager::write_close();
 
 // check if execution allowed
 if (!empty($CFG->cronclionly)) {
index 1f186b6..3c0cb6c 100644 (file)
@@ -163,7 +163,7 @@ if (!core_tables_exist()) {
     $strinstallation = get_string('installation', 'install');
 
     // remove current session content completely
-    session_get_instance()->terminate_current();
+    \core\session\manager::terminate_current();
 
     if (empty($agreelicense)) {
         $strlicense = get_string('license');
index 3a58d34..ea2491a 100644 (file)
@@ -35,7 +35,9 @@ $ADMIN->add('server', $temp);
 
 // "sessionhandling" settingpage
 $temp = new admin_settingpage('sessionhandling', new lang_string('sessionhandling', 'admin'));
-$temp->add(new admin_setting_configcheckbox('dbsessions', new lang_string('dbsessions', 'admin'), new lang_string('configdbsessions', 'admin'), 1));
+if (empty($CFG->session_handler_class) and $DB->session_lock_supported()) {
+    $temp->add(new admin_setting_configcheckbox('dbsessions', new lang_string('dbsessions', 'admin'), new lang_string('configdbsessions', 'admin'), 0));
+}
 $temp->add(new admin_setting_configselect('sessiontimeout', new lang_string('sessiontimeout', 'admin'), new lang_string('configsessiontimeout', 'admin'), 7200, array(14400 => new lang_string('numhours', '', 4),
                                                                                                                                                       10800 => new lang_string('numhours', '', 3),
                                                                                                                                                       7200 => new lang_string('numhours', '', 2),
index bab7a94..0d92dc0 100644 (file)
@@ -47,7 +47,7 @@ if (!$confirm) {
 }
 raise_memory_limit(MEMORY_EXTRA);
 // Release session.
-session_get_instance()->write_close();
+\core\session\manager::write_close();
 
 echo $renderer->header();
 echo $renderer->heading(get_string('batchupgrade', 'tool_assignmentupgrade'));
index 190d86f..3357365 100644 (file)
@@ -52,7 +52,7 @@ require_once($CFG->libdir.'/dtllib.php');
 function tool_dbtransfer_export_xml_database($description, $mdb) {
     @set_time_limit(0);
 
-    session_get_instance()->write_close(); // Release session.
+    \core\session\manager::write_close(); // Release session.
 
     header('Content-Type: application/xhtml+xml; charset=utf-8');
     header('Content-Disposition: attachment; filename=database.xml');
@@ -79,7 +79,7 @@ function tool_dbtransfer_export_xml_database($description, $mdb) {
 function tool_dbtransfer_transfer_database(moodle_database $sourcedb, moodle_database $targetdb, progress_trace $feedback = null) {
     @set_time_limit(0);
 
-    session_get_instance()->write_close(); // Release session.
+    \core\session\manager::write_close(); // Release session.
 
     $var = new database_mover($sourcedb, $targetdb, true, $feedback);
     $var->export_database(null);
index 407a58f..940e753 100644 (file)
@@ -90,7 +90,7 @@ if ($error = tool_generator_course_backend::check_shortname_available($shortname
 }
 
 // Switch to admin user account.
-session_set_user(get_admin());
+\core\session\manager::set_user(get_admin());
 
 // Do backend code to generate course.
 $backend = new tool_generator_course_backend($shortname, $size, $fixeddataset, empty($options['quiet']));
index bc91d7d..1e2a7d4 100644 (file)
@@ -88,7 +88,7 @@ try {
 }
 
 // Switch to admin user account.
-session_set_user(get_admin());
+\core\session\manager::set_user(get_admin());
 
 // Do backend code to generate site.
 $backend = new tool_generator_site_backend($size, $options['bypasscheck'], $fixeddataset, empty($options['quiet']));
index 7949d94..509374c 100644 (file)
@@ -687,7 +687,7 @@ if ($formdata = $mform2->is_cancelled()) {
             }
 
             if ($dologout) {
-                session_kill_user($existinguser->id);
+                \core\session\manager::kill_user_sessions($existinguser->id);
             }
 
         } else {
index 699babb..45ddc21 100644 (file)
             die;
         } else if (data_submitted() and !$user->deleted) {
             if (delete_user($user)) {
-                session_gc(); // remove stale sessions
+                \core\session\manager::gc(); // Remove stale sessions.
                 redirect($returnurl);
             } else {
-                session_gc(); // remove stale sessions
+                \core\session\manager::gc(); // Remove stale sessions.
                 echo $OUTPUT->header();
                 echo $OUTPUT->notification($returnurl, get_string('deletednot', '', fullname($user, true)));
             }
             if (!is_siteadmin($user) and $USER->id != $user->id and $user->suspended != 1) {
                 $user->suspended = 1;
                 // Force logout.
-                session_kill_user($user->id);
+                \core\session\manager::kill_user_sessions($user->id);
                 user_update_user($user, false);
             }
         }
index 4dbb75d..474903a 100644 (file)
@@ -34,7 +34,7 @@ if ($confirm and confirm_sesskey()) {
         }
     }
     $rs->close();
-    session_gc(); // remove stale sessions
+    \core\session\manager::gc(); // Remove stale sessions.
     echo $OUTPUT->box_start('generalbox', 'notice');
     if (!empty($notifications)) {
         echo $notifications;
index a435caa..0f80f7d 100644 (file)
@@ -808,7 +808,7 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $updateuser->suspended = 1;
                     user_update_user($updateuser, false);
                     echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
-                    session_kill_user($user->id);
+                    \core\session\manager::kill_user_sessions($user->id);
                 }
             } else {
                 print_string('nouserentriestoremove', 'auth_ldap');
index e21d77a..e995021 100644 (file)
@@ -141,7 +141,7 @@ class auth_plugin_mnet extends auth_plugin_base {
         global $CFG, $USER, $DB;
         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
 
-        if (session_is_loggedinas()) {
+        if (\core\session\manager::is_loggedinas()) {
             print_error('notpermittedtojumpas', 'mnet');
         }
 
@@ -919,7 +919,7 @@ class auth_plugin_mnet extends auth_plugin_base {
                 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
             } else {
                 foreach($results as $emigrant) {
-                    session_touch($emigrant->session_id);
+                    \core\session\manager::touch_session($emigrant->session_id);
                 }
             }
         }
@@ -1076,7 +1076,7 @@ class auth_plugin_mnet extends auth_plugin_base {
                                  array('useragent'=>$useragent, 'userid'=>$userid));
 
         if (isset($remoteclient) && isset($remoteclient->id)) {
-            session_kill_user($userid);
+            \core\session\manager::kill_user_sessions($userid);
         }
         return $returnstring;
     }
@@ -1096,7 +1096,7 @@ class auth_plugin_mnet extends auth_plugin_base {
         $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
         $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
         if (false != $session) {
-            session_kill($session->session_id);
+            \core\session\manager::kill_session($session->session_id);
             return true;
         }
         return false;
@@ -1113,7 +1113,7 @@ class auth_plugin_mnet extends auth_plugin_base {
         global $CFG;
         if (is_array($sessionArray)) {
             while($session = array_pop($sessionArray)) {
-                session_kill($session->session_id);
+                \core\session\manager::kill_session($session->session_id);
             }
             return true;
         }
index 61f6ba8..3eaf702 100644 (file)
@@ -48,7 +48,7 @@
                 && $user = authenticate_user_login($frm->username, $frm->password)) {
 
             enrol_check_plugins($user);
-            session_set_user($user);
+            \core\session\manager::set_user($user);
 
             $USER->loggedin = true;
             $USER->site     = $CFG->wwwroot; // for added security, store the site in the
index 76e8c26..29c1f1d 100644 (file)
@@ -34,7 +34,7 @@ $PAGE->set_url('/badges/ajax.php');
 $PAGE->set_context(context_system::instance());
 
 // Unlock session during potentially long curl request.
-session_get_instance()->write_close();
+\core\session\manager::write_close();
 
 $result = badges_check_backpack_accessibility();
 
index 58de649..9890661 100644 (file)
@@ -81,7 +81,7 @@ function block_html_pluginfile($course, $birecord_or_cm, $context, $filearea, $a
         $forcedownload = true;
     }
 
-    session_get_instance()->write_close();
+    \core\session\manager::write_close();
     send_stored_file($file, 60*60, 0, $forcedownload, $options);
 }
 
index 2021fef..e776a45 100644 (file)
@@ -25,7 +25,7 @@ class block_mnet_hosts extends block_list {
             return false;
         }
 
-        if (session_is_loggedinas()) {
+        if (\core\session\manager::is_loggedinas()) {
             $this->content = new stdClass();
             $this->content->footer = html_writer::tag('span',
                 get_string('notpermittedtojumpas', 'mnet'));
index 10e72ea..9f11799 100644 (file)
@@ -277,6 +277,6 @@ class core_calendar_type_testcase extends advanced_testcase {
      */
     private function set_calendar_type($type) {
         $this->user->calendartype = $type;
-        session_set_user($this->user);
+        \core\session\manager::set_user($this->user);
     }
 }
index 1865166..59830d9 100644 (file)
@@ -224,10 +224,22 @@ $CFG->admin = 'admin';
 // RewriteRule (^.*/theme/yui_combo\.php)(/.*) $1?file=$2
 //
 //
-// By default all user sessions should be using locking, uncomment
-// the following setting to prevent locking for guests and not-logged-in
-// accounts. This may improve performance significantly.
-//     $CFG->sessionlockloggedinonly = 1;
+// Following settings may be used to select session driver, uncomment only one of the handlers.
+//   Database session handler (not compatible with MyISAM):
+//      $CFG->session_handler_class = '\core\session\database';
+//      $CFG->session_database_acquire_lock_timeout = 120;
+//
+//   File session handler (file system locking required):
+//      $CFG->session_handler_class = '\core\session\file';
+//      $CFG->session_file_save_path = $CFG->dataroot.'/sessions';
+//
+//   Memcached session handler (requires memcached server and extension):
+//      $CFG->session_handler_class = '\core\session\memcached';
+//      $CFG->session_memcached_save_path = '127.0.0.1:11211';
+//      $CFG->session_memcached_prefix = 'memc.sess.key.';
+//
+// Following setting allows you to alter how frequently is timemodified updated in sessions table.
+//      $CFG->session_update_timemodified_frequency = 20; // In seconds.
 //
 // If this setting is set to true, then Moodle will track the IP of the
 // current user to make sure it hasn't changed during a session.  This
index 97f7d2a..bfc3495 100644 (file)
@@ -11,7 +11,7 @@ $url = new moodle_url('/course/loginas.php', array('id'=>$id));
 $PAGE->set_url($url);
 
 // Reset user back to their real self if needed, for security reasons you need to log out and log in again.
-if (session_is_loggedinas()) {
+if (\core\session\manager::is_loggedinas()) {
     require_sesskey();
     require_logout();
 
@@ -61,7 +61,7 @@ if (has_capability('moodle/user:loginas', $systemcontext)) {
 }
 
 // Login as this user and return to course home page.
-session_loginas($userid, $context);
+\core\session\manager::loginas($userid, $context);
 $newfullname = fullname($USER, true);
 
 $strloginas    = get_string('loginas');
index 821ae57..ce61d19 100644 (file)
@@ -84,5 +84,5 @@ if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->get_filename() ==
 // ========================================
 // finally send the file
 // ========================================
-session_get_instance()->write_close(); // unlock session during fileserving
+\core\session\manager::write_close(); // Unlock session during file serving.
 send_stored_file($file, 0, false, true, array('preview' => $preview)); // force download - security first!
index d0e91b5..3cc5335 100644 (file)
@@ -46,7 +46,7 @@ $PAGE->set_pagelayout('course');
 $PAGE->set_url('/enrol/index.php', array('id'=>$course->id));
 
 // do not allow enrols when in login-as session
-if (session_is_loggedinas() and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
+if (\core\session\manager::is_loggedinas() and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
     print_error('loginasnoenrol', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
 }
 
index fce3f78..bfbe870 100644 (file)
--- a/file.php
+++ b/file.php
@@ -111,7 +111,7 @@ if ($file->get_filename() == '.') {
 // ========================================
 // finally send the file
 // ========================================
-session_get_instance()->write_close(); // unlock session during fileserving
+\core\session\manager::write_close(); // Unlock session during file serving.
 send_stored_file($file, $lifetime, $CFG->filteruploadedfiles, $forcedownload);
 
 
index 2f18aa6..d615046 100644 (file)
@@ -466,6 +466,7 @@ $string['servicedonotexist'] = 'The service does not exist';
 $string['sessionwaiterr'] = 'Timed out while waiting for session lock.<br />Wait for your current requests to finish and try again later.';
 $string['sessioncookiesdisable'] = 'Incorrect use of require_key_login() - session cookies must be disabled!';
 $string['sessiondiskfull'] = 'The session partition is full. It is not possible to login at this time.<br /><br />Please notify server administrator.';
+$string['sessionhandlerproblem'] = 'Session handler is misconfigured';
 $string['sessionerroruser'] = 'Your session has timed out.  Please login again.';
 $string['sessionerroruser2'] = 'A server error that affects your login session was detected. Please login again or restart your browser.';
 $string['sessionipnomatch'] = 'Sorry, but your IP number seems to have changed from when you first logged in.  This security feature prevents crackers stealing your identity while logged in to this site.  Normal users should not be seeing this message - please ask the site administrator for help.';
index f8fac6e..7363742 100644 (file)
@@ -618,9 +618,7 @@ function login_is_lockedout($user) {
 function login_attempt_valid($user) {
     global $CFG;
 
-    $event = \core\event\user_loggedin::create(array('objectid' => $user->id, 'other' => array('username' => $user->username)));
-    $event->add_record_snapshot('user', $user);
-    $event->trigger();
+    // Note: user_loggedin event is triggered in complete_user_login().
 
     if ($user->mnethostid != $CFG->mnet_localhost_id) {
         return;
index 006485f..9065aa7 100644 (file)
@@ -96,7 +96,7 @@ class user_loggedin extends \core\event\base {
     /**
      * Custom validation.
      *
-     * @throws coding_exception when validation does not pass.
+     * @throws \coding_exception when validation does not pass.
      * @return void
      */
     protected function validate_data() {
index f881b47..8f2c6aa 100644 (file)
@@ -38,6 +38,7 @@ class user_loggedout extends base {
      * Initialise required event data properties.
      */
     protected function init() {
+        $this->context = \context_system::instance();
         $this->data['objecttable'] = 'user';
         $this->data['crud'] = 'r';
         $this->data['level'] = self::LEVEL_OTHER;
diff --git a/lib/classes/session/database.php b/lib/classes/session/database.php
new file mode 100644 (file)
index 0000000..4216980
--- /dev/null
@@ -0,0 +1,315 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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
+ */
+
+namespace core\session;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 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 {
+    /** @var \stdClass $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;
+        }
+    }
+
+    /**
+     * Init session handler.
+     */
+    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(array($this, 'handler_open'),
+            array($this, 'handler_close'),
+            array($this, 'handler_read'),
+            array($this, 'handler_write'),
+            array($this, 'handler_destroy'),
+            array($this, 'handler_gc'));
+        if (!$result) {
+            throw new exception('dbsessionhandlerproblem', 'error');
+        }
+
+        register_shutdown_function(array($this, 'handler_shutdown'));
+    }
+
+    /**
+     * Check for existing session with id $sid.
+     *
+     * Note: this verifies the storage backend only, not the actual session records.
+     *
+     * @param string $sid
+     * @return bool true if session found.
+     */
+    public function session_exists($sid) {
+        try {
+            return $this->database->record_exists('sessions', array('sid'=>$sid, 'state'=>0));
+        } catch (\dml_exception $ex) {
+            return false;
+        }
+    }
+
+    /**
+     * Kill all active sessions, the core sessions table is
+     * purged afterwards.
+     */
+    public function kill_all_sessions() {
+        // Nothing to do, the sessions table is cleared from core.
+        return;
+    }
+
+    /**
+     * Kill one session, the session record is removed afterwards.
+     * @param string $sid
+     */
+    public function kill_session($sid) {
+        // Nothing to do, the sessions table is purged afterwards.
+        return;
+    }
+
+    /**
+     * Open session handler.
+     *
+     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
+     *
+     * @param string $save_path
+     * @param string $session_name
+     * @return bool success
+     */
+    public function handler_open($save_path, $session_name) {
+        // 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 handler_close() {
+        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
+     */
+    public function handler_read($sid) {
+        try {
+            if (!$record = $this->database->get_record('sessions', array('sid'=>$sid), 'id')) {
+                // 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.
+                $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 $sid
+     * @param string $session_data
+     * @return bool success
+     */
+    public function handler_write($sid, $session_data) {
+        if ($this->failed) {
+            // Do not write anything back - we failed to start the session properly.
+            return false;
+        }
+
+        $sessdata = base64_encode($session_data); // There might be some binary mess :-(
+        $hash = sha1($sessdata);
+
+        if ($hash === $this->lasthash) {
+            return true;
+        }
+
+        try {
+            if ($this->recordid) {
+                $this->database->set_field('sessions', 'sessdata', $sessdata, array('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, array('sid'=>$sid));
+            }
+        } catch (\Exception $ex) {
+            // Do not rethrow exceptions here, this should not happen.
+            error_log('Unknown exception when writing database session data : '.$sid.' - '.$ex->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Destroy session handler.
+     *
+     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
+     *
+     * @param string $sid
+     * @return bool success
+     */
+    public function handler_destroy($sid) {
+        if (!$session = $this->database->get_record('sessions', array('sid'=>$sid), 'id, sid')) {
+            if ($sid == session_id()) {
+                $this->recordid = null;
+                $this->lasthash = null;
+            }
+            return true;
+        }
+
+        if ($this->recordid and $session->id == $this->recordid) {
+            try {
+                $this->database->release_session_lock($this->recordid);
+            } catch (\Exception $ex) {
+                // Ignore problems.
+            }
+            $this->recordid = null;
+            $this->lasthash = null;
+        }
+
+        $this->database->delete_records('sessions', array('id'=>$session->id));
+
+        return true;
+    }
+
+    /**
+     * GC session handler.
+     *
+     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
+     *
+     * @param int $ignored_maxlifetime moodle uses special timeout rules
+     * @return bool success
+     */
+    public function handler_gc($ignored_maxlifetime) {
+        // This should do something only if cron is not running properly...
+        if (!$stalelifetime = ini_get('session.gc_maxlifetime')) {
+            return true;
+        }
+        $params = array('purgebefore' => (time() - $stalelifetime));
+        $this->database->delete_records_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params);
+        return true;
+    }
+
+    /**
+     * This makes sure the session is written to disk at the end of request.
+     */
+    public function handler_shutdown() {
+        $this->database->dispose();
+    }
+}
diff --git a/lib/classes/session/exception.php b/lib/classes/session/exception.php
new file mode 100644 (file)
index 0000000..ea58b0f
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 {
+}
diff --git a/lib/classes/session/file.php b/lib/classes/session/file.php
new file mode 100644 (file)
index 0000000..7ecb00c
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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
+ */
+
+namespace core\session;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 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";
+        }
+    }
+
+    /**
+     * Init session handler.
+     */
+    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).
+        $freespace = @disk_free_space($this->sessiondir);
+        if (!($freespace > 2048) and $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);
+    }
+
+    /**
+     * Check for existing session with id $sid.
+     *
+     * Note: this verifies the storage backend only, not the actual session records.
+     *
+     * @param string $sid
+     * @return bool true if session found.
+     */
+    public function session_exists($sid) {
+        $sid = clean_param($sid, PARAM_FILE);
+        if (!$sid) {
+            return false;
+        }
+        $sessionfile = "$this->sessiondir/sess_$sid";
+        return file_exists($sessionfile);
+    }
+
+    /**
+     * Kill all active sessions, the core sessions table is
+     * purged afterwards.
+     */
+    public function kill_all_sessions() {
+        if (is_dir($this->sessiondir)) {
+            foreach (glob("$this->sessiondir/sess_*") as $filename) {
+                @unlink($filename);
+            }
+        }
+    }
+
+    /**
+     * Kill one session, the session record is removed afterwards.
+     * @param string $sid
+     */
+    public function kill_session($sid) {
+        $sid = clean_param($sid, PARAM_FILE);
+        if (!$sid) {
+            return;
+        }
+        $sessionfile = "$this->sessiondir/sess_$sid";
+        if (file_exists($sessionfile)) {
+            @unlink($sessionfile);
+        }
+    }
+}
diff --git a/lib/classes/session/handler.php b/lib/classes/session/handler.php
new file mode 100644 (file)
index 0000000..a656175
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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
+ */
+
+namespace core\session;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 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 {
+    /**
+     * Init session handler.
+     */
+    public abstract function init();
+
+    /**
+     * Check for existing session with id $sid.
+     *
+     * Note: this verifies the storage backend only, not the actual session records.
+     *
+     * @param string $sid
+     * @return bool true if session found.
+     */
+    public abstract function session_exists($sid);
+
+    /**
+     * Kill all active sessions, the core sessions table is
+     * purged afterwards.
+     */
+    public abstract function kill_all_sessions();
+
+    /**
+     * Kill one session, the session record is removed afterwards.
+     * @param string $sid
+     */
+    public abstract function kill_session($sid);
+}
diff --git a/lib/classes/session/manager.php b/lib/classes/session/manager.php
new file mode 100644 (file)
index 0000000..90153ef
--- /dev/null
@@ -0,0 +1,754 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Session manager class.
+ *
+ * @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 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 handler $handler active session handler instance */
+    protected static $handler;
+
+    /** @var bool $sessionactive Is the session active? */
+    protected static $sessionactive = null;
+
+    /**
+     * Start user session.
+     *
+     * Note: This is intended to be called only from lib/setup.php!
+     */
+    public static function start() {
+        global $CFG, $DB;
+
+        if (isset(self::$sessionactive)) {
+            debugging('Session was already started!', DEBUG_DEVELOPER);
+            return;
+        }
+
+        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;
+        }
+
+        try {
+            self::$handler->init();
+            self::prepare_cookies();
+            $newsid = empty($_COOKIE[session_name()]);
+
+            session_start();
+
+            self::initialise_user_session($newsid);
+            self::check_security();
+
+        } catch (\Exception $ex) {
+            @session_write_close();
+            self::init_empty_session();
+            self::$sessionactive = false;
+            throw $ex;
+        }
+
+        self::$sessionactive = true;
+    }
+
+    /**
+     * Returns current page performance info.
+     *
+     * @return array perf info
+     */
+    public static function get_performance_info() {
+        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'] = "<span class=\"sessionsize\">Session ($handler): $size</span> ";
+        $info['txt'] = "Session ($handler): $size ";
+
+        return $info;
+    }
+
+    /**
+     * Create handler instance.
+     */
+    protected static function load_handler() {
+        global $CFG, $DB;
+
+        if (self::$handler) {
+            return;
+        }
+
+        // Find out which handler to use.
+        if (PHPUNIT_TEST) {
+            $class = '\core\session\file';
+
+        } else if (!empty($CFG->session_handler_class)) {
+            $class = $CFG->session_handler_class;
+
+        } else if (!empty($CFG->dbsessions) and $DB->session_lock_supported()) {
+            $class = '\core\session\database';
+
+        } else {
+            $class = '\core\session\file';
+        }
+        self::$handler = new $class();
+    }
+
+    /**
+     * Empty current session, fill it with not-logged-in user info.
+     */
+    protected static function init_empty_session() {
+        global $CFG;
+
+        // Session not used at all.
+        $_SESSION = array();
+        $_SESSION['SESSION']  = new \stdClass();
+        $_SESSION['USER']     = new \stdClass();
+        $_SESSION['USER']->id = 0;
+        if (isset($CFG->mnet_localhost_id)) {
+            $_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
+        } else {
+            // Not installed yet, the future host id will be most probably 1.
+            $_SESSION['USER']->mnethostid = 1;
+        }
+
+        if (PHPUNIT_TEST) {
+            // Phpunit tests use reversed reference.
+            global $USER, $SESSION;
+            $USER = $_SESSION['USER'];
+            $SESSION = $_SESSION['SESSION'];
+            $_SESSION['USER'] =& $USER;
+            $_SESSION['SESSION'] =& $SESSION;
+        }
+    }
+
+    /**
+     * Make sure all cookie and session related stuff is configured properly before session start.
+     */
+    protected static function prepare_cookies() {
+        global $CFG;
+
+        if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
+            $CFG->cookiesecure = 0;
+        }
+
+        if (!isset($CFG->cookiehttponly)) {
+            $CFG->cookiehttponly = 0;
+        }
+
+        // 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);
+        session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
+        ini_set('session.use_trans_sid', '0');
+        ini_set('session.use_only_cookies', '1');
+        ini_set('session.hash_function', '0');        // For now MD5 - we do not have room for sha-1 in sessions table.
+        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 $USER and $SESSION objects, handles google access
+     * and sets up not-logged-in user properly.
+     *
+     * @param bool $newsid is this a new session in first http request?
+     */
+    protected static function initialise_user_session($newsid) {
+        global $CFG, $DB;
+
+        $sid = session_id();
+        if (!$sid) {
+            // No session, very weird.
+            error_log('Missing session ID, session not started!');
+            self::init_empty_session();
+            return;
+        }
+
+        if (!$record = $DB->get_record('sessions', array('sid'=>$sid), 'id, sid, state, userid, lastip, timecreated, timemodified')) {
+            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.");
+                }
+            }
+            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 < 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) {
+                session_regenerate_id(true);
+                $_SESSION = array();
+                $DB->delete_records('sessions', array('id'=>$record->id));
+
+            } 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;
+                }
+
+                $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) {
+                    $update->id = $record->id;
+                    $DB->update_record('sessions', $update);
+                }
+
+                return;
+            }
+        } else {
+            if ($record) {
+                // This happens when people switch session handlers...
+                session_regenerate_id(true);
+                $_SESSION = array();
+                $DB->delete_records('sessions', array('id'=>$record->id));
+            }
+        }
+        unset($record);
+
+        $timedout = false;
+        if (!isset($_SESSION['SESSION'])) {
+            $_SESSION['SESSION'] = new \stdClass();
+            if (!$newsid) {
+                $timedout = true;
+            }
+        }
+
+        $user = null;
+
+        if (!empty($CFG->opentogoogle)) {
+            if (is_web_crawler()) {
+                $user = guest_user();
+            }
+            if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
+                // Automatically log in users coming from search engine results.
+                if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
+                    $user = guest_user();
+                } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
+                    $user = guest_user();
+                }
+            }
+        }
+
+        // Setup $USER and insert the session tracking record.
+        if ($user) {
+            self::set_user($user);
+            self::add_session_record($user->id);
+        } else {
+            self::init_empty_session();
+            self::add_session_record(0);
+        }
+
+        if ($timedout) {
+            $_SESSION['SESSION']->has_timed_out = true;
+        }
+    }
+
+    /**
+     * Insert new empty session record.
+     * @param int $userid
+     * @return \stdClass the new record
+     */
+    protected static function add_session_record($userid) {
+        global $DB;
+        $record = new \stdClass();
+        $record->state       = 0;
+        $record->sid         = session_id();
+        $record->sessdata    = null;
+        $record->userid      = $userid;
+        $record->timecreated = $record->timemodified = time();
+        $record->firstip     = $record->lastip = getremoteaddr();
+
+        $record->id = $DB->insert_record('sessions', $record);
+
+        return $record;
+    }
+
+    /**
+     * Do various session security checks.
+     */
+    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);
+        $DB->delete_records('sessions', array('sid'=>$sid));
+        self::add_session_record($user->id);
+
+        // Let enrol plugins deal with new enrolments if necessary.
+        enrol_check_plugins($user);
+
+        // Setup $USER object.
+        self::set_user($user);
+    }
+
+    /**
+     * 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);
+        $DB->delete_records('sessions', array('sid'=>$sid));
+        self::init_empty_session();
+        self::add_session_record($_SESSION['USER']->id);
+        session_write_close();
+        self::$sessionactive = false;
+    }
+
+    /**
+     * No more changes in session expected.
+     * Unblocks the sessions, other scripts may start executing in parallel.
+     */
+    public static function write_close() {
+        if (self::$sessionactive) {
+            session_write_close();
+        } else {
+            if (session_id()) {
+                @session_write_close();
+            }
+        }
+        self::$sessionactive = false;
+    }
+
+    /**
+     * Does the PHP session with given id exist?
+     *
+     * Note: this does not actually verify the presence of sessions record.
+     *
+     * @param string $sid
+     * @return bool
+     */
+    public static function session_exists($sid) {
+        self::load_handler();
+        return self::$handler->session_exists($sid);
+    }
+
+    /**
+     * Fake last access for given session, this prevents session timeout.
+     * @param string $sid
+     */
+    public static function touch_session($sid) {
+        global $DB;
+
+        // Timeouts depend on core sessions table only, no need to update anything in external stores.
+
+        $sql = "UPDATE {sessions} SET timemodified = :now WHERE sid = :sid";
+        $DB->execute($sql, array('now'=>time(), 'sid'=>$sid));
+    }
+
+    /**
+     * Terminate all sessions unconditionally.
+     */
+    public static function kill_all_sessions() {
+        global $DB;
+
+        self::terminate_current();
+
+        self::load_handler();
+        self::$handler->kill_all_sessions();
+
+        try {
+            $DB->delete_records('sessions');
+        } catch (\dml_exception $ignored) {
+            // Do not show any warnings - might be during upgrade/installation.
+        }
+    }
+
+    /**
+     * Terminate give session unconditionally.
+     * @param string $sid
+     */
+    public static function kill_session($sid) {
+        global $DB;
+
+        self::load_handler();
+
+        if ($sid === session_id()) {
+            self::write_close();
+        }
+
+        self::$handler->kill_session($sid);
+
+        $DB->delete_records('sessions', array('sid'=>$sid));
+    }
+
+    /**
+     * Terminate all sessions of given user unconditionally.
+     * @param int $userid
+     */
+    public static function kill_user_sessions($userid) {
+        global $DB;
+
+        $sessions = $DB->get_records('sessions', array('userid'=>$userid), 'id DESC', 'id, sid');
+        foreach ($sessions as $session) {
+            self::kill_session($session->sid);
+        }
+    }
+
+    /**
+     * Set current user.
+     *
+     * @param \stdClass $user record
+     */
+    public static function set_user(\stdClass $user) {
+        $_SESSION['USER'] = $user;
+        unset($_SESSION['USER']->description); // Conserve memory.
+        unset($_SESSION['USER']->password);    // Improve security.
+        if (isset($_SESSION['USER']->lang)) {
+            // Make sure it is a valid lang pack name.
+            $_SESSION['USER']->lang = clean_param($_SESSION['USER']->lang, PARAM_LANG);
+        }
+        sesskey(); // Init session key.
+
+        if (PHPUNIT_TEST) {
+            // Phpunit tests use reversed reference.
+            global $USER;
+            $USER = $_SESSION['USER'];
+            $_SESSION['USER'] =& $USER;
+        }
+    }
+
+    /**
+     * Periodic timed-out session cleanup.
+     */
+    public static function gc() {
+        global $CFG, $DB;
+
+        // This may take a long time...
+        set_time_limit(0);
+
+        $maxlifetime = $CFG->sessiontimeout;
+
+        try {
+            // Kill all sessions of deleted and suspended users without any hesitation.
+            $rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0 OR suspended <> 0)", array(), 'id DESC', 'id, sid');
+            foreach ($rs as $session) {
+                self::kill_session($session->sid);
+            }
+            $rs->close();
+
+            // Kill sessions of users with disabled plugins.
+            $auth_sequence = get_enabled_auth_plugins(true);
+            $auth_sequence = array_flip($auth_sequence);
+            unset($auth_sequence['nologin']); // No login means user cannot login.
+            $auth_sequence = array_flip($auth_sequence);
+
+            list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
+            $rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params, 'id DESC', 'id, sid');
+            foreach ($rs as $session) {
+                self::kill_session($session->sid);
+            }
+            $rs->close();
+
+            // Now get a list of time-out candidates - real users only.
+            $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 = array('purgebefore' => (time() - $maxlifetime), 'guestid'=>$CFG->siteguest);
+
+            $authplugins = array();
+            foreach ($auth_sequence as $authname) {
+                $authplugins[$authname] = get_auth_plugin($authname);
+            }
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $user) {
+                foreach ($authplugins as $authplugin) {
+                    /** @var \auth_plugin_base $authplugin*/
+                    if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
+                        continue;
+                    }
+                }
+                self::kill_session($user->sid);
+            }
+            $rs->close();
+
+            // Delete expired sessions for guest user account, give them larger timeout, there is no security risk here.
+            $params = array('purgebefore' => (time() - ($maxlifetime * 5)), 'guestid'=>$CFG->siteguest);
+            $rs = $DB->get_recordset_select('sessions', 'userid = :guestid AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid');
+            foreach ($rs as $session) {
+                self::kill_session($session->sid);
+            }
+            $rs->close();
+
+            // Delete expired sessions for userid = 0 (not logged in), better kill them asap to release memory.
+            $params = array('purgebefore' => (time() - $maxlifetime));
+            $rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid');
+            foreach ($rs as $session) {
+                self::kill_session($session->sid);
+            }
+            $rs->close();
+
+            // Cleanup letfovers from the first browser access because it may set multiple cookies and then use only one.
+            $params = array('purgebefore' => (time() - 60*3));
+            $rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified = timecreated AND timemodified < :purgebefore', $params, 'id ASC', 'id, sid');
+            foreach ($rs as $session) {
+                self::kill_session($session->sid);
+            }
+            $rs->close();
+
+        } catch (\Exception $ex) {
+            debugging('Error gc-ing sessions: '.$ex->getMessage(), DEBUG_NORMAL, $ex->getTrace());
+        }
+    }
+
+    /**
+     * Is current $USER logged-in-as somebody else?
+     * @return bool
+     */
+    public static function is_loggedinas() {
+        return !empty($_SESSION['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 $_SESSION['USER'];
+        }
+    }
+
+    /**
+     * Login as another user - no security checks here.
+     * @param int $userid
+     * @param \context $context
+     * @return void
+     */
+    public static function loginas($userid, \context $context) {
+        global $USER;
+
+        if (self::is_loggedinas()) {
+            return;
+        }
+
+        // Switch to fresh new $SESSION.
+        $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
+        $_SESSION['SESSION']     = new \stdClass();
+
+        // Create the new $USER object with all details and reload needed capabilities.
+        $_SESSION['REALUSER'] = $_SESSION['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);
+
+        // 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);
+        $event->trigger();
+    }
+}
diff --git a/lib/classes/session/memcached.php b/lib/classes/session/memcached.php
new file mode 100644 (file)
index 0000000..7281537
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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
+ */
+
+namespace core\session;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 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;
+
+    /**
+     * 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_servers($this->savepath);
+        }
+
+        if (empty($CFG->session_memcached_prefix)) {
+            $this->prefix = ini_get('memcached.sess_prefix');
+        } else {
+            $this->prefix = $CFG->session_memcached_prefix;
+        }
+    }
+
+    /**
+     * Init session handler.
+     */
+    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');
+        }
+
+        // NOTE: we cannot set any lock acquiring timeout here - bad luck.
+        ini_set('session.save_handler', 'memcached');
+        ini_set('session.save_path', $this->savepath);
+        ini_set('memcached.sess_prefix', $this->prefix);
+        ini_set('memcached.sess_locking', '1'); // Locking is required!
+    }
+
+    /**
+     * Check for existing session with id $sid.
+     *
+     * Note: this verifies the storage backend only, not the actual session records.
+     *
+     * @param string $sid
+     * @return bool true if session found.
+     */
+    public function session_exists($sid) {
+        if (!$this->servers) {
+            return false;
+        }
+
+        $memcached = new \Memcached();
+        $memcached->addServers($this->servers);
+        $value = $memcached->get($this->prefix.$sid);
+        $memcached->quit();
+
+        return ($value !== false);
+    }
+
+    /**
+     * Kill all active sessions, the core sessions table is
+     * purged afterwards.
+     */
+    public function kill_all_sessions() {
+        global $DB;
+        if (!$this->servers) {
+            return;
+        }
+
+        $memcached = new \Memcached();
+        $memcached->addServers($this->servers);
+
+        // 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) {
+            $memcached->delete($this->prefix.$record->sid);
+        }
+        $rs->close();
+
+        $memcached->quit();
+    }
+
+    /**
+     * Kill one session, the session record is removed afterwards.
+     * @param string $sid
+     */
+    public function kill_session($sid) {
+        if (!$this->servers) {
+            return;
+        }
+
+        $memcached = new \Memcached();
+        $memcached->addServers($this->servers);
+        $memcached->delete($this->prefix.$sid);
+
+        $memcached->quit();
+    }
+
+    /**
+     * Convert a connection string to an array of servers
+     *
+     * EG: Converts: "abc:123, xyz:789" to
+     *
+     *  array(
+     *      array('abc', '123'),
+     *      array('xyz', '789'),
+     *  )
+     *
+     * @copyright  2013 Moodlerooms Inc. (http://www.moodlerooms.com)
+     * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+     * @author     Mark Nielsen
+     *
+     * @param string $str save_path value containing memcached connection string
+     * @return array
+     */
+    protected static function connection_string_to_servers($str) {
+        $servers = array();
+        $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[] = array($host, $port);
+        }
+        return $servers;
+    }
+}
index b15105a..0f95c75 100644 (file)
@@ -190,9 +190,10 @@ function cron_run() {
     mtrace(' Created missing context instances');
 
 
-    // Session gc
-    session_gc();
-    mtrace("Cleaned up stale user sessions");
+    // Session gc.
+    mtrace("Running session gc tasks...");
+    \core\session\manager::gc();
+    mtrace("...finished stale session cleanup");
 
 
     // Run the auth cron, if any before enrolments
index f2044c3..3509b44 100644 (file)
@@ -1645,7 +1645,7 @@ function add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user
     if ($user) {
         $userid = $user;
     } else {
-        if (session_is_loggedinas()) {  // Don't log
+        if (\core\session\manager::is_loggedinas()) {  // Don't log
             return;
         }
         $userid = empty($USER->id) ? '0' : $USER->id;
@@ -1727,7 +1727,7 @@ function add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user
 function user_accesstime_log($courseid=0) {
     global $USER, $CFG, $DB;
 
-    if (!isloggedin() or session_is_loggedinas()) {
+    if (!isloggedin() or \core\session\manager::is_loggedinas()) {
         // no access tracking
         return;
     }
index 3cc26ed..6ef7851 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Factory method that was returning moodle_session object.
+ *
+ * @deprecated since 2.6
+ * @return \core\session\manager
+ */
+function session_get_instance() {
+    // Note: the new session manager includes all methods from the original session class.
+    static $deprecatedinstance = null;
+
+    debugging('session_get_instance() is deprecated, use \core\session\manager instead', DEBUG_DEVELOPER);
+
+    if (!$deprecatedinstance) {
+        $deprecatedinstance = new \core\session\manager();
+    }
+
+    return $deprecatedinstance;
+}
+
+/**
+ * Returns true if legacy session used.
+ *
+ * @deprecated since 2.6
+ * @return bool
+ */
+function session_is_legacy() {
+    debugging('session_is_legacy() is deprecated, do not use any more', DEBUG_DEVELOPER);
+    return false;
+}
+
+/**
+ * Terminates all sessions, auth hooks are not executed.
+ * Useful in upgrade scripts.
+ *
+ * @deprecated since 2.6
+ */
+function session_kill_all() {
+    debugging('session_kill_all() is deprecated, use \core\session\manager::kill_all_sessions() instead', DEBUG_DEVELOPER);
+    \core\session\manager::kill_all_sessions();
+}
+
+/**
+ * Mark session as accessed, prevents timeouts.
+ *
+ * @deprecated since 2.6
+ * @param string $sid
+ */
+function session_touch($sid) {
+    debugging('session_touch() is deprecated, use \core\session\manager::touch_session() instead', DEBUG_DEVELOPER);
+    \core\session\manager::touch_session($sid);
+}
+
+/**
+ * Terminates one sessions, auth hooks are not executed.
+ *
+ * @deprecated since 2.6
+ * @param string $sid session id
+ */
+function session_kill($sid) {
+    debugging('session_kill() is deprecated, use \core\session\manager::kill_session() instead', DEBUG_DEVELOPER);
+    \core\session\manager::kill_session($sid);
+}
+
+/**
+ * Terminates all sessions of one user, auth hooks are not executed.
+ * NOTE: This can not work for file based sessions!
+ *
+ * @deprecated since 2.6
+ * @param int $userid user id
+ */
+function session_kill_user($userid) {
+    debugging('session_kill_user() is deprecated, use \core\session\manager::kill_user_sessions() instead', DEBUG_DEVELOPER);
+    \core\session\manager::kill_user_sessions($userid);
+}
+
+/**
+ * Session garbage collection
+ * - verify timeout for all users
+ * - kill sessions of all deleted users
+ * - kill sessions of users with disabled plugins or 'nologin' plugin
+ *
+ * @deprecated since 2.6
+ */
+function session_gc() {
+    debugging('session_gc() is deprecated, use \core\session\manager::gc() instead', DEBUG_DEVELOPER);
+    \core\session\manager::gc();
+}
+
+/**
+ * Setup $USER object - called during login, loginas, etc.
+ *
+ * Call sync_user_enrolments() manually after log-in, or log-in-as.
+ *
+ * @deprecated since 2.6
+ * @param stdClass $user full user record object
+ * @return void
+ */
+function session_set_user($user) {
+    debugging('session_set_user() is deprecated, use \core\session\manager::set_user() instead', DEBUG_DEVELOPER);
+    \core\session\manager::set_user($user);
+}
+
+/**
+ * Is current $USER logged-in-as somebody else?
+ * @deprecated since 2.6
+ * @return bool
+ */
+function session_is_loggedinas() {
+    debugging('session_is_loggedinas() is deprecated, use \core\session\manager::is_loggedinas() instead', DEBUG_DEVELOPER);
+    return \core\session\manager::is_loggedinas();
+}
+
+/**
+ * Returns the $USER object ignoring current login-as session
+ * @deprecated since 2.6
+ * @return stdClass user object
+ */
+function session_get_realuser() {
+    debugging('session_get_realuser() is deprecated, use \core\session\manager::get_realuser() instead', DEBUG_DEVELOPER);
+    return \core\session\manager::get_realuser();
+}
+
+/**
+ * Login as another user - no security checks here.
+ * @deprecated since 2.6
+ * @param int $userid
+ * @param stdClass $context
+ * @return void
+ */
+function session_loginas($userid, $context) {
+    debugging('session_loginas() is deprecated, use \core\session\manager::loginas() instead', DEBUG_DEVELOPER);
+    \core\session\manager::loginas($userid, $context);
+}
+
 /**
  * Minify JavaScript files.
  *
index c9ab87c..8152487 100644 (file)
@@ -119,6 +119,12 @@ abstract class moodle_database {
     /** @var string MD5 of settings used for connection. Used by MUC as an identifier. */
     private $settingshash;
 
+    /** @var cache_application for column info */
+    protected $metacache;
+
+    /** @var bool flag marking database instance as disposed */
+    protected $disposed;
+
     /**
      * @var int internal temporary variable used to fix params. Its used by {@link _fix_sql_params_dollar_callback()}.
      */
@@ -337,6 +343,10 @@ abstract class moodle_database {
      * @return void
      */
     public function dispose() {
+        if ($this->disposed) {
+            return;
+        }
+        $this->disposed = true;
         if ($this->transactions) {
             // this should not happen, it usually indicates wrong catching of exceptions,
             // because all transactions should be finished manually or in default exception handler.
@@ -354,9 +364,7 @@ abstract class moodle_database {
         }
         // Always terminate sessions here to make it consistent,
         // this is needed because we need to save session to db before closing it.
-        if (function_exists('session_get_instance')) {
-            session_get_instance()->write_close();
-        }
+        \core\session\manager::write_close();
         $this->used_for_db_sessions = false;
 
         if ($this->temptables) {
@@ -368,6 +376,10 @@ abstract class moodle_database {
             $this->database_manager = null;
         }
         $this->tables  = null;
+
+        // We do not need the MUC cache any more,
+        // if we did not keep it as property it might be already gone before we saved the session.
+        $this->metacache = null;
     }
 
     /**
@@ -955,12 +967,27 @@ abstract class moodle_database {
      * @return void
      */
     public function reset_caches() {
-        $this->tables  = null;
+        $this->tables = null;
+        $this->metacache = null;
         // Purge MUC as well
         $identifiers = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
         cache_helper::purge_by_definition('core', 'databasemeta', $identifiers);
     }
 
+    /**
+     * Call before using $this->metacache.
+     *
+     * Note: this is necessary because we want to write to database in shutdown handler
+     *       and it needs to use the caches, but MUC would be already disposed.
+     */
+    protected function init_caches() {
+        if ($this->metacache) {
+            return;
+        }
+        $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
+        $this->metacache = cache::make('core', 'databasemeta', $properties);
+    }
+
     /**
      * Returns the sql generator used for db manipulation.
      * Used mostly in upgrade.php scripts.
index b4def7a..88c3575 100644 (file)
@@ -398,9 +398,8 @@ class mssql_native_moodle_database extends moodle_database {
     public function get_columns($table, $usecache=true) {
 
         if ($usecache) {
-            $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
-            $cache = cache::make('core', 'databasemeta', $properties);
-            if ($data = $cache->get($table)) {
+            $this->init_caches();
+            if ($data = $this->metacache->get($table)) {
                 return $data;
             }
         }
@@ -496,7 +495,7 @@ class mssql_native_moodle_database extends moodle_database {
         $this->free_result($result);
 
         if ($usecache) {
-            $result = $cache->set($table, $structure);
+            $result = $this->metacache->set($table, $structure);
         }
 
         return $structure;
index 7a2d31f..f80c479 100644 (file)
@@ -511,9 +511,8 @@ class mysqli_native_moodle_database extends moodle_database {
     public function get_columns($table, $usecache=true) {
 
         if ($usecache) {
-            $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
-            $cache = cache::make('core', 'databasemeta', $properties);
-            if ($data = $cache->get($table)) {
+            $this->init_caches();
+            if ($data = $this->metacache->get($table)) {
                 return $data;
             }
         }
@@ -619,7 +618,7 @@ class mysqli_native_moodle_database extends moodle_database {
         }
 
         if ($usecache) {
-            $result = $cache->set($table, $structure);
+            $result = $this->metacache->set($table, $structure);
         }
 
         return $structure;
index 10906b8..5f412ef 100644 (file)
@@ -470,9 +470,8 @@ class oci_native_moodle_database extends moodle_database {
     public function get_columns($table, $usecache=true) {
 
         if ($usecache) {
-            $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
-            $cache = cache::make('core', 'databasemeta', $properties);
-            if ($data = $cache->get($table)) {
+            $this->init_caches();
+            if ($data = $this->metacache->get($table)) {
                 return $data;
             }
         }
@@ -664,7 +663,7 @@ class oci_native_moodle_database extends moodle_database {
         }
 
         if ($usecache) {
-            $result = $cache->set($table, $structure);
+            $result = $this->metacache->set($table, $structure);
         }
 
         return $structure;
@@ -1722,6 +1721,10 @@ class oci_native_moodle_database extends moodle_database {
         }
     }
 
+    public function session_lock_supported() {
+        return true;
+    }
+
     /**
      * Obtain session lock
      * @param int $rowid id of the row with session record
index 7c9792e..3ae868d 100644 (file)
@@ -385,9 +385,8 @@ class pgsql_native_moodle_database extends moodle_database {
      */
     public function get_columns($table, $usecache=true) {
         if ($usecache) {
-            $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
-            $cache = cache::make('core', 'databasemeta', $properties);
-            if ($data = $cache->get($table)) {
+            $this->init_caches();
+            if ($data = $this->metacache->get($table)) {
                 return $data;
             }
         }
@@ -570,7 +569,7 @@ class pgsql_native_moodle_database extends moodle_database {
         pg_free_result($result);
 
         if ($usecache) {
-            $result = $cache->set($table, $structure);
+            $result = $this->metacache->set($table, $structure);
         }
 
         return $structure;
index d5a0080..c711197 100644 (file)
@@ -201,9 +201,8 @@ class sqlite3_pdo_moodle_database extends pdo_moodle_database {
     public function get_columns($table, $usecache=true) {
 
         if ($usecache) {
-            $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
-            $cache = cache::make('core', 'databasemeta', $properties);
-            if ($data = $cache->get($table)) {
+            $this->init_caches();
+            if ($data = $this->metacache->get($table)) {
                 return $data;
             }
         }
@@ -300,7 +299,7 @@ class sqlite3_pdo_moodle_database extends pdo_moodle_database {
         }
 
         if ($usecache) {
-            $result = $cache->set($table, $structure);
+            $result = $this->metacache->set($table, $structure);
         }
 
         return $structure;
index b939e9f..3d45697 100644 (file)
@@ -462,9 +462,8 @@ class sqlsrv_native_moodle_database extends moodle_database {
      */
     public function get_columns($table, $usecache = true) {
         if ($usecache) {
-            $properties = array('dbfamily' => $this->get_dbfamily(), 'settings' => $this->get_settings_hash());
-            $cache = cache::make('core', 'databasemeta', $properties);
-            if ($data = $cache->get($table)) {
+            $this->init_caches();
+            if ($data = $this->metacache->get($table)) {
                 return $data;
             }
         }
@@ -560,7 +559,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
         $this->free_result($result);
 
         if ($usecache) {
-            $result = $cache->set($table, $structure);
+            $result = $this->metacache->set($table, $structure);
         }
 
         return $structure;
index cf82585..8127f40 100644 (file)
@@ -2171,7 +2171,7 @@ function send_temp_file($path, $filename, $pathisstring=false) {
     }
 
     // close session - not needed anymore
-    session_get_instance()->write_close();
+    \core\session\manager::write_close();
 
     if (!$pathisstring) {
         if (!file_exists($path)) {
@@ -2254,7 +2254,7 @@ function send_file($path, $filename, $lifetime = 'default' , $filter=0, $pathiss
         }
     }
 
-    session_get_instance()->write_close(); // unlock session during fileserving
+    \core\session\manager::write_close(); // Unlock session during file serving.
 
     // Use given MIME type if specified, otherwise guess it using mimeinfo.
     // IE, Konqueror and Opera open html file directly in browser from web even when directed to save it to disk :-O
@@ -2417,7 +2417,7 @@ function send_stored_file($stored_file, $lifetime=86400 , $filter=0, $forcedownl
         ignore_user_abort(true);
     }
 
-    session_get_instance()->write_close(); // unlock session during fileserving
+    \core\session\manager::write_close(); // Unlock session during file serving.
 
     // Use given MIME type if specified, otherwise guess it using mimeinfo.
     // IE, Konqueror and Opera open html file directly in browser from web even when directed to save it to disk :-O
@@ -3918,7 +3918,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'feedback' and $context->contextlevel == CONTEXT_COURSE) {
@@ -3935,7 +3935,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
         } else {
             send_file_not_found();
@@ -3956,7 +3956,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, true, array('preview' => $preview));
 
         } else {
@@ -3978,14 +3978,14 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close();
+            \core\session\manager::write_close();
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
         } else if ($filearea === 'userbadge'  and $context->contextlevel == CONTEXT_USER) {
             if (!$file = $fs->get_file($context->id, 'badges', 'userbadge', $badge->id, '/', $filename.'.png')) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close();
+            \core\session\manager::write_close();
             send_stored_file($file, 60*60, 0, true, array('preview' => $preview));
         }
     // ========================================================================================================================
@@ -4012,7 +4012,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_USER) {
@@ -4040,7 +4040,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSE) {
@@ -4087,7 +4087,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else {
@@ -4161,7 +4161,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security!
 
         } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_USER) {
@@ -4208,7 +4208,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security!
 
         } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_COURSE) {
@@ -4246,7 +4246,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security!
 
         } else if ($filearea === 'backup' and $context->contextlevel == CONTEXT_USER) {
@@ -4267,7 +4267,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 0, 0, true, array('preview' => $preview)); // must force download - security!
 
         } else {
@@ -4292,7 +4292,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
         } else {
             send_file_not_found();
@@ -4315,7 +4315,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'section') {
@@ -4337,7 +4337,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else {
@@ -4369,7 +4369,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'icon') {
@@ -4384,7 +4384,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 }
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, false, array('preview' => $preview));
 
         } else {
@@ -4409,7 +4409,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else {
@@ -4428,7 +4428,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 0, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'section' and $context->contextlevel == CONTEXT_COURSE) {
@@ -4443,7 +4443,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close();
+            \core\session\manager::write_close();
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'activity' and $context->contextlevel == CONTEXT_MODULE) {
@@ -4456,7 +4456,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close();
+            \core\session\manager::write_close();
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
 
         } else if ($filearea === 'automated' and $context->contextlevel == CONTEXT_COURSE) {
@@ -4471,7 +4471,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 0, 0, $forcedownload, array('preview' => $preview));
 
         } else {
@@ -4517,7 +4517,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
-            session_get_instance()->write_close(); // unlock session during fileserving
+            \core\session\manager::write_close(); // Unlock session during file serving.
             send_stored_file($file, 60*60, 0, $forcedownload, array('preview' => $preview));
         }
 
index f60605b..40705f1 100644 (file)
@@ -495,7 +495,7 @@ function install_cli_database(array $options, $interactive) {
     upgrade_finished();
 
     // log in as admin - we need do anything when applying defaults
-    session_set_user(get_admin());
+    \core\session\manager::set_user(get_admin());
 
     // apply all default settings, do it twice to fill all defaults - some settings depend on other setting
     admin_apply_default_settings(NULL, true);
index 92091ea..a7708cf 100644 (file)
@@ -1582,7 +1582,7 @@ function get_users_from_config($value, $capability, $includeadmins = true) {
  * @return void
  */
 function purge_all_caches() {
-    global $CFG;
+    global $CFG, $DB;
 
     reset_text_filters_cache();
     js_reset_all_caches();
@@ -1597,6 +1597,7 @@ function purge_all_caches() {
         // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet.
     }
 
+    $DB->reset_caches();
     cache_helper::purge_all();
 
     // Purge all other caches: rss, simplepie, etc.
@@ -2896,7 +2897,7 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
     }
 
     // Loginas as redirection if needed.
-    if ($course->id != SITEID and session_is_loggedinas()) {
+    if ($course->id != SITEID and \core\session\manager::is_loggedinas()) {
         if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) {
             if ($USER->loginascontext->instanceid != $course->id) {
                 print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
@@ -2905,7 +2906,7 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
     }
 
     // Check whether the user should be changing password (but only if it is REALLY them).
-    if (get_user_preferences('auth_forcepasswordchange') && !session_is_loggedinas()) {
+    if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) {
         $userauth = get_auth_plugin($USER->auth);
         if ($userauth->can_change_password() and !$preventredirect) {
             if ($setwantsurltome) {
@@ -3013,9 +3014,9 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
     if ($course->id == SITEID) {
         // Everybody is enrolled on the frontpage.
     } else {
-        if (session_is_loggedinas()) {
+        if (\core\session\manager::is_loggedinas()) {
             // Make sure the REAL person can access this course first.
-            $realuser = session_get_realuser();
+            $realuser = \core\session\manager::get_realuser();
             if (!is_enrolled($coursecontext, $realuser->id, '', true) and
                 !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) {
                 if ($preventredirect) {
@@ -3147,26 +3148,39 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
  * @category   access
  */
 function require_logout() {
-    global $USER;
+    global $USER, $DB;
 
-    if (isloggedin()) {
-        $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
-        foreach ($authsequence as $authname) {
-            $authplugin = get_auth_plugin($authname);
-            $authplugin->prelogout_hook();
-        }
+    if (!isloggedin()) {
+        // This should not happen often, no need for hooks or events here.
+        \core\session\manager::terminate_current();
+        return;
     }
 
+    // Execute hooks before action.
+    $authsequence = get_enabled_auth_plugins();
+    foreach ($authsequence as $authname) {
+        $authplugin = get_auth_plugin($authname);
+        $authplugin->prelogout_hook();
+    }
+
+    // Store info that gets removed during logout.
+    $sid = session_id();
     $event = \core\event\user_loggedout::create(
-            array(
-                'objectid' => $USER->id,
-                'context' => context_user::instance($USER->id)
-                )
-            );
-    $event->trigger();
+        array(
+            'userid' => $USER->id,
+            'objectid' => $USER->id,
+            'other' => array('sessionid' => $sid),
+        )
+    );
+    if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
+        $event->add_record_snapshot('sessions', $session);
+    }
 
-    session_get_instance()->terminate_current();
-    unset($GLOBALS['USER']);
+    // Delete session record and drop $_SESSION content.
+    \core\session\manager::terminate_current();
+
+    // Trigger event AFTER action.
+    $event->trigger();
 }
 
 /**
@@ -3271,7 +3285,7 @@ function require_user_key_login($script, $instance=null) {
     }
 
     // Extra safety.
-    @session_write_close();
+    \core\session\manager::write_close();
 
     $keyvalue = required_param('key', PARAM_ALPHANUM);
 
@@ -3296,7 +3310,7 @@ function require_user_key_login($script, $instance=null) {
 
     // Emulate normal session.
     enrol_check_plugins($user);
-    session_set_user($user);
+    \core\session\manager::set_user($user);
 
     // Note we are not using normal login.
     if (!defined('USER_KEY_LOGIN')) {
@@ -4154,7 +4168,7 @@ function delete_user(stdClass $user) {
     $DB->delete_records('user_private_key', array('userid' => $user->id));
 
     // Force logout - may fail if file based sessions used, sorry.
-    session_kill_user($user->id);
+    \core\session\manager::kill_user_sessions($user->id);
 
     // Workaround for bulk deletes of users with the same email address.
     $delname = "$user->email.".time();
@@ -4386,15 +4400,7 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
 function complete_user_login($user) {
     global $CFG, $USER;
 
-    // Regenerate session id and delete old session,
-    // this helps prevent session fixation attacks from the same domain.
-    session_regenerate_id(true);
-
-    // Let enrol plugins deal with new enrolments if necessary.
-    enrol_check_plugins($user);
-
-    // Check enrolments, load caps and setup $USER object.
-    session_set_user($user);
+    \core\session\manager::login_user($user);
 
     // Reload preferences from DB.
     unset($USER->preference);
@@ -4406,11 +4412,27 @@ function complete_user_login($user) {
     // Extra session prefs init.
     set_login_session_preferences();
 
+    // Trigger login event.
+    $event = \core\event\user_loggedin::create(
+        array(
+            'userid' => $USER->id,
+            'objectid' => $USER->id,
+            'other' => array('username' => $USER->username),
+        )
+    );
+    $event->add_record_snapshot('user', $user);
+    $event->trigger();
+
     if (isguestuser()) {
         // No need to continue when user is THE guest.
         return $USER;
     }
 
+    if (CLI_SCRIPT) {
+        // We can redirect to password change URL only in browser.
+        return $USER;
+    }
+
     // Select password change url.
     $userauth = get_auth_plugin($USER->auth);
 
@@ -8834,10 +8856,10 @@ function get_performance_info() {
     }
 
     // Display size of session if session started.
-    if (session_id()) {
-        $info['sessionsize'] = display_size(strlen(session_encode()));
-        $info['html'] .= '<span class="sessionsize">Session: ' . $info['sessionsize'] . '</span> ';
-        $info['txt'] .= "Session: {$info['sessionsize']} ";
+    if ($si = \core\session\manager::get_performance_info()) {
+        $info['sessionsize'] = $si['size'];
+        $info['html'] .= $si['html'];
+        $info['txt'] .= $si['txt'];
     }
 
     if ($stats = cache_helper::get_stats()) {
index bf958a6..506d2d7 100644 (file)
@@ -3347,9 +3347,9 @@ class settings_navigation extends navigation_node {
         }
 
         // Check if the user is currently logged in as another user
-        if (session_is_loggedinas()) {
+        if (\core\session\manager::is_loggedinas()) {
             // Get the actual user, we need this so we can display an informative return link
-            $realuser = session_get_realuser();
+            $realuser = \core\session\manager::get_realuser();
             // Add the informative return to original user link
             $url = new moodle_url('/course/loginas.php',array('id'=>$this->page->course->id, 'return'=>1,'sesskey'=>sesskey()));
             $this->add(get_string('returntooriginaluser', 'moodle', fullname($realuser, true)), $url, self::TYPE_SETTING, null, null, new pix_icon('t/left', ''));
@@ -4075,7 +4075,7 @@ class settings_navigation extends navigation_node {
         }
 
         // Change password link
-        if ($userauthplugin && $currentuser && !session_is_loggedinas() && !isguestuser() && has_capability('moodle/user:changeownpassword', $systemcontext) && $userauthplugin->can_change_password()) {
+        if ($userauthplugin && $currentuser && !\core\session\manager::is_loggedinas() && !isguestuser() && has_capability('moodle/user:changeownpassword', $systemcontext) && $userauthplugin->can_change_password()) {
             $passwordchangeurl = $userauthplugin->change_password_url();
             if (empty($passwordchangeurl)) {
                 $passwordchangeurl = new moodle_url('/login/change_password.php', array('id'=>$course->id));
@@ -4193,7 +4193,7 @@ class settings_navigation extends navigation_node {
         $reporttab->trim_if_empty();
 
         // Login as ...
-        if (!$user->deleted and !$currentuser && !session_is_loggedinas() && has_capability('moodle/user:loginas', $coursecontext) && !is_siteadmin($user->id)) {
+        if (!$user->deleted and !$currentuser && !\core\session\manager::is_loggedinas() && has_capability('moodle/user:loginas', $coursecontext) && !is_siteadmin($user->id)) {
             $url = new moodle_url('/course/loginas.php', array('id'=>$course->id, 'user'=>$user->id, 'sesskey'=>sesskey()));
             $usersetting->add(get_string('loginas'), $url, self::TYPE_SETTING);
         }
index 35173a8..15de22b 100644 (file)
@@ -578,8 +578,8 @@ class core_renderer extends renderer_base {
 
         $loginpage = ((string)$this->page->url === get_login_url());
         $course = $this->page->course;
-        if (session_is_loggedinas()) {
-            $realuser = session_get_realuser();
+        if (\core\session\manager::is_loggedinas()) {
+            $realuser = \core\session\manager::get_realuser();
             $fullname = fullname($realuser, true);
             if ($withlinks) {
                 $loginastitle = get_string('loginas');
@@ -775,7 +775,7 @@ class core_renderer extends renderer_base {
     public function header() {
         global $USER, $CFG;
 
-        if (session_is_loggedinas()) {
+        if (\core\session\manager::is_loggedinas()) {
             $this->page->add_body_class('userloggedinas');
         }
 
index 4b60ec3..42d4dd3 100644 (file)
@@ -422,7 +422,7 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
         unset($user->access);
         unset($user->preference);
 
-        session_set_user($user);
+        \core\session\manager::set_user($user);
     }
 
     /**
index a48f71d..b0c5bed 100644 (file)
@@ -191,7 +191,7 @@ class phpunit_util extends testing_util {
         $user = new stdClass();
         $user->id = 0;
         $user->mnethostid = $CFG->mnet_localhost_id;
-        session_set_user($user);
+        \core\session\manager::set_user($user);
 
         // reset all static caches
         \core\event\manager::phpunit_reset();
index fba7d04..52328af 100644 (file)
@@ -89,7 +89,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         $this->assertEquals(3, $_SESSION['USER']->id);
         $this->assertSame($_SESSION['USER'], $USER);
 
-        session_set_user($user);
+        \core\session\manager::set_user($user);
         $this->assertEquals(2, $USER->id);
         $this->assertEquals(2, $_SESSION['USER']->id);
         $this->assertSame($_SESSION['USER'], $USER);
index 8063106..49b542d 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-if (!defined('SESSION_ACQUIRE_LOCK_TIMEOUT')) {
-    /**
-     * How much time to wait for session lock before displaying error (in seconds),
-     * 2 minutes by default should be a reasonable time before telling users to wait and refresh browser.
-     */
-    define('SESSION_ACQUIRE_LOCK_TIMEOUT', 60*2);
-}
-
-/**
-  * Factory method returning moodle_session object.
-  * @return moodle_session
-  */
-function session_get_instance() {
-    global $CFG, $DB;
-
-    static $session = null;
-
-    if (is_null($session)) {
-        if (!defined('NO_MOODLE_COOKIES') or empty($DB)) {
-            // Moodle was not initialised properly in lib/setup.php.
-            $session = new emergency_session();
-            return $session;
-        }
-
-        if (empty($CFG->sessiontimeout)) {
-            $CFG->sessiontimeout = 7200;
-        }
-
-        try {
-            if (defined('SESSION_CUSTOM_CLASS')) {
-                // this is a hook for webservices, key based login, etc.
-                if (defined('SESSION_CUSTOM_FILE')) {
-                    require_once($CFG->dirroot.SESSION_CUSTOM_FILE);
-                }
-                $session_class = SESSION_CUSTOM_CLASS;
-                $session = new $session_class();
-
-            } else if ((!isset($CFG->dbsessions) or $CFG->dbsessions) and $DB->session_lock_supported()) {
-                // default recommended session type
-                $session = new database_session();
-
-            } else {
-                // legacy limited file based storage - some features and auth plugins will not work, sorry
-                $session = new legacy_file_session();
-            }
-        } catch (Exception $ex) {
-            // prevent repeated inits
-            $session = new emergency_session();
-            throw $ex;
-        }
-    }
-
-    return $session;
-}
-
-
-/**
- * Moodle session abstraction
- *
- * @package    core
- * @subpackage session
- * @copyright  2008 Petr Skoda  {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-interface moodle_session {
-    /**
-     * Terminate current session
-     * @return void
-     */
-    public function terminate_current();
-
-    /**
-     * No more changes in session expected.
-     * Unblocks the sessions, other scripts may start executing in parallel.
-     * @return void
-     */
-    public function write_close();
-
-    /**
-     * Check for existing session with id $sid
-     * @param unknown_type $sid
-     * @return boolean true if session found.
-     */
-    public function session_exists($sid);
-}
-
-
-/**
- * Fallback session handler when standard session init fails.
- * This prevents repeated attempts to init faulty handler.
- *
- * @package    core
- * @subpackage session
- * @copyright  2011 Petr Skoda  {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class emergency_session implements moodle_session {
-
-    public function __construct() {
-        // session not used at all
-        $_SESSION = array();
-        $_SESSION['SESSION'] = new stdClass();
-        $_SESSION['USER']    = new stdClass();
-    }
-
-    /**
-     * Terminate current session
-     * @return void
-     */
-    public function terminate_current() {
-        return;
-    }
-
-    /**
-     * No more changes in session expected.
-     * Unblocks the sessions, other scripts may start executing in parallel.
-     * @return void
-     */
-    public function write_close() {
-        return;
-    }
-
-    /**
-     * Check for existing session with id $sid
-     * @param unknown_type $sid
-     * @return boolean true if session found.
-     */
-    public function session_exists($sid) {
-        return false;
-    }
-}
-
-
-/**
- * Class handling all session and cookies related stuff.
- *
- * @package    core
- * @subpackage session
- * @copyright  2009 Petr Skoda  {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-abstract class session_stub implements moodle_session {
-    protected $justloggedout;
-
-    public function __construct() {
-        global $CFG;
-
-        if (NO_MOODLE_COOKIES) {
-            // session not used at all
-            $_SESSION = array();
-            $_SESSION['SESSION'] = new stdClass();
-            $_SESSION['USER']    = new stdClass();
-
-        } else {
-            $this->prepare_cookies();
-            $this->init_session_storage();
-
-            $newsession = empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
-
-            ini_set('session.use_trans_sid', '0');
-
-            session_name('MoodleSession'.$CFG->sessioncookie);
-            session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
-            session_start();
-            if (!isset($_SESSION['SESSION'])) {
-                $_SESSION['SESSION'] = new stdClass();
-                if (!$newsession and !$this->justloggedout) {
-                    $_SESSION['SESSION']->has_timed_out = true;
-                }
-            }
-            if (!isset($_SESSION['USER'])) {
-                $_SESSION['USER'] = new stdClass();
-            }
-        }
-
-        $this->check_user_initialised();
-
-        $this->check_security();
-    }
-
-    /**
-     * Terminate current session
-     * @return void
-     */
-    public function terminate_current() {
-        global $CFG, $SESSION, $USER, $DB;
-
-        try {
-            $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
-        } catch (Exception $ignored) {
-            // probably install/upgrade - ignore this problem
-        }
-
-        if (NO_MOODLE_COOKIES) {
-            return;
-        }
-
-        // Initialize variable to pass-by-reference to headers_sent(&$file, &$line)
-        $_SESSION = array();
-        $_SESSION['SESSION'] = new stdClass();
-        $_SESSION['USER']    = new stdClass();
-        $_SESSION['USER']->id = 0;
-        if (isset($CFG->mnet_localhost_id)) {
-            $_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
-        }
-        $SESSION = $_SESSION['SESSION']; // this may not work properly
-        $USER    = $_SESSION['USER'];    // this may not work properly
-
-        $file = null;
-        $line = null;
-        if (headers_sent($file, $line)) {
-            error_log('Can not terminate session properly - headers were already sent in file: '.$file.' on line '.$line);
-        }
-
-        // now let's try to get a new session id and delete the old one
-        $this->justloggedout = true;
-        session_regenerate_id(true);
-        $this->justloggedout = false;
-
-        // write the new session
-        session_write_close();
-    }
-
-    /**
-     * No more changes in session expected.
-     * Unblocks the sessions, other scripts may start executing in parallel.
-     * @return void
-     */
-    public function write_close() {
-        if (NO_MOODLE_COOKIES) {
-            return;
-        }
-
-        session_write_close();
-    }
-
-    /**
-     * Initialise $USER object, handles google access
-     * and sets up not logged in user properly.
-     *
-     * @return void
-     */
-    protected function check_user_initialised() {
-        global $CFG;
-
-        if (isset($_SESSION['USER']->id)) {
-            // already set up $USER
-            return;
-        }
-
-        $user = null;
-
-        if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
-            if (is_web_crawler()) {
-                $user = guest_user();
-            }
-            if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
-                // automaticaly log in users coming from search engine results
-                if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
-                    $user = guest_user();
-                } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
-                    $user = guest_user();
-                }
-            }
-        }
-
-        if (!$user) {
-            $user = new stdClass();
-            $user->id = 0; // to enable proper function of $CFG->notloggedinroleid hack
-            if (isset($CFG->mnet_localhost_id)) {
-                $user->mnethostid = $CFG->mnet_localhost_id;
-            } else {
-                $user->mnethostid = 1;
-            }
-        }
-        session_set_user($user);
-    }
-
-    /**
-     * Does various session security checks
-     * @global void
-     */
-    protected function check_security() {
-        global $CFG;
-
-        if (NO_MOODLE_COOKIES) {
-            return;
-        }
-
-        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
-                $this->terminate_current();
-                print_error('sessionipnomatch2', 'error');
-            }
-        }
-    }
-
-    /**
-     * Prepare cookies and various system settings
-     */
-    protected function prepare_cookies() {
-        global $CFG;
-
-        if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
-            $CFG->cookiesecure = 0;
-        }
-
-        if (!isset($CFG->cookiehttponly)) {
-            $CFG->cookiehttponly = 0;
-        }
-
-    /// Set sessioncookie and sessioncookiepath variable if it isn't already
-        if (!isset($CFG->sessioncookie)) {
-            $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(${'MoodleSession'.$CFG->sessioncookie});
-        unset($_GET['MoodleSession'.$CFG->sessioncookie]);
-        unset($_POST['MoodleSession'.$CFG->sessioncookie]);
-        unset($_REQUEST['MoodleSession'.$CFG->sessioncookie]);
-
-        //compatibility hack for Moodle Cron, cookies not deleted, but set to "deleted" - should not be needed with NO_MOODLE_COOKIES in cron.php now
-        if (!empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]) && $_COOKIE['MoodleSession'.$CFG->sessioncookie] == "deleted") {
-            unset($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
-        }
-    }
-
-    /**
-     * Init session storage.
-     */
-    protected abstract function init_session_storage();
-}
-
-
-/**
- * Legacy moodle sessions stored in files, not recommended any more.
- *
- * @package    core
- * @subpackage session
- * @copyright  2009 Petr Skoda  {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class legacy_file_session extends session_stub {
-    /**
-     * Init session storage.
-     */
-    protected function init_session_storage() {
-        global $CFG;
-
-        ini_set('session.save_handler', 'files');
-
-        // Some distros disable GC by setting probability to 0
-        // overriding the PHP default of 1
-        // (gc_probability is divided by gc_divisor, which defaults to 1000)
-        if (ini_get('session.gc_probability') == 0) {
-            ini_set('session.gc_probability', 1);
-        }
-
-        ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
-
-        // make sure sessions dir exists and is writable, throws exception if not
-        make_upload_directory('sessions');
-
-        // Need to disable debugging since disk_free_space()
-        // will fail on very large partitions (see MDL-19222)
-        $freespace = @disk_free_space($CFG->dataroot.'/sessions');
-        if (!($freespace > 2048) and $freespace !== false) {
-            print_error('sessiondiskfull', 'error');
-        }
-        ini_set('session.save_path', $CFG->dataroot .'/sessions');
-    }
-    /**
-     * Check for existing session with id $sid
-     * @param unknown_type $sid
-     * @return boolean true if session found.
-     */
-    public function session_exists($sid){
-        global $CFG;
-
-        $sid = clean_param($sid, PARAM_FILE);
-        $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
-        return file_exists($sessionfile);
-    }
-}
-
-
-/**
- * Recommended moodle session storage.
- *
- * @package    core
- * @subpackage session
- * @copyright  2009 Petr Skoda  {@link http://skodak.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class database_session extends session_stub {
-    /** @var stdClass $record session record */
-    protected $record   = 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 hash of the session data content */
-    protected $lasthash = null;
-
-    public function __construct() {
-        global $DB;
-        $this->database = $DB;
-        parent::__construct();
-
-        if (!empty($this->record->state)) {
-            // something is very wrong
-            session_kill($this->record->sid);
-
-            if ($this->record->state == 9) {
-                print_error('dbsessionmysqlpacketsize', 'error');
-            }
-        }
-    }
-
-    /**
-     * Check for existing session with id $sid
-     * @param string $sid
-     * @return boolean true if session found.
-     */
-    public function session_exists($sid){
-        global $CFG;
-        try {
-            $sql = "SELECT * FROM {sessions} WHERE timemodified < ? AND sid=? AND state=?";
-            $params = array(time() + $CFG->sessiontimeout, $sid, 0);
-
-            return $this->database->record_exists_sql($sql, $params);
-        } catch (dml_exception $ex) {
-            error_log('Error checking existance of database session');
-            return false;
-        }
-    }
-
-    /**
-     * Init session storage.
-     */
-    protected function init_session_storage() {
-        global $CFG;
-
-        // gc only from CRON - individual user timeouts now checked during each access
-        ini_set('session.gc_probability', 0);
-
-        ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
-
-        $result = session_set_save_handler(array($this, 'handler_open'),
-                                           array($this, 'handler_close'),
-                                           array($this, 'handler_read'),
-                                           array($this, 'handler_write'),
-                                           array($this, 'handler_destroy'),
-                                           array($this, 'handler_gc'));
-        if (!$result) {
-            print_error('dbsessionhandlerproblem', 'error');
-        }
-    }
-
-    /**
-     * Open session handler
-     *
-     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
-     *
-     * @param string $save_path
-     * @param string $session_name
-     * @return bool success
-     */
-    public function handler_open($save_path, $session_name) {
-        return true;
-    }
-
-    /**
-     * Close session handler
-     *
-     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
-     *
-     * @return bool success
-     */
-    public function handler_close() {
-        if (isset($this->record->id)) {
-            try {
-                $this->database->release_session_lock($this->record->id);
-            } catch (Exception $ex) {
-                // ignore any problems
-            }
-        }
-        $this->record = null;
-        return true;
-    }
-
-    /**
-     * Read session handler
-     *
-     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
-     *
-     * @param string $sid
-     * @return string
-     */
-    public function handler_read($sid) {
-        global $CFG;
-
-        if ($this->record and $this->record->sid != $sid) {
-            error_log('Weird error reading database session - mismatched sid');
-            $this->failed = true;
-            return '';
-        }
-
-        try {
-            // Do not fetch full record yet, wait until it is locked.
-            if (!$record = $this->database->get_record('sessions', array('sid'=>$sid), 'id, userid')) {
-                $record = new stdClass();
-                $record->state        = 0;
-                $record->sid          = $sid;
-                $record->sessdata     = null;
-                $record->userid       = 0;
-                $record->timecreated  = $record->timemodified = time();
-                $record->firstip      = $record->lastip = getremoteaddr();
-                $record->id           = $this->database->insert_record_raw('sessions', $record);
-            }
-        } catch (Exception $ex) {
-            // do not rethrow exceptions here, we need this to work somehow before 1.9.x upgrade and during install
-            error_log('Can not read or insert database sessions');
-            $this->failed = true;
-            return '';
-        }
-
-        try {
-            if (!empty($CFG->sessionlockloggedinonly) and (isguestuser($record->userid) or empty($record->userid))) {
-                // No session locking for guests and not-logged-in users,
-                // these users mostly read stuff, there should not be any major
-                // session race conditions. Hopefully they do not access other
-                // pages while being logged-in.
-            } else {
-                $this->database->get_session_lock($record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
-            }
-        } catch (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 soon after access control checks
-            error_log('Can not obtain session lock');
-            $this->failed = true;
-            throw $ex;
-        }
-
-        // 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))) {
-            error_log('Cannot read session record');
-            $this->failed = true;
-            return '';
-        }
-
-        // verify timeout
-        if ($record->timemodified + $CFG->sessiontimeout < time()) {
-            $ignoretimeout = false;
-            if (!empty($record->userid)) { // skips not logged in
-                if ($user = $this->database->get_record('user', array('id'=>$record->userid))) {
-
-                    // Refresh session if logged as a guest
-                    if (isguestuser($user)) {
-                        $ignoretimeout = true;
-                    } else {
-                        $authsequence = get_enabled_auth_plugins(); // auths, in sequence
-                        foreach($authsequence as $authname) {
-                            $authplugin = get_auth_plugin($authname);
-                            if ($authplugin->ignore_timeout_hook($user, $record->sid, $record->timecreated, $record->timemodified)) {
-                                $ignoretimeout = true;
-                                break;
-                            }
-                        }
-                    }
-                }
-            }
-            if ($ignoretimeout) {
-                //refresh session
-                $record->timemodified = time();
-                try {
-                    $this->database->update_record('sessions', $record);
-                } catch (Exception $ex) {
-                    // very unlikely error
-                    error_log('Can not refresh database session');
-                    $this->failed = true;
-                    throw $ex;
-                }
-            } else {
-                //time out session
-                $record->state        = 0;
-                $record->sessdata     = null;
-                $record->userid       = 0;
-                $record->timecreated  = $record->timemodified = time();
-                $record->firstip      = $record->lastip = getremoteaddr();
-                try {
-                    $this->database->update_record('sessions', $record);
-                } catch (Exception $ex) {
-                    // very unlikely error
-                    error_log('Can not time out database session');
-                    $this->failed = true;
-                    throw $ex;
-                }
-            }
-        }
-
-        if (is_null($record->sessdata)) {
-            $data = '';
-            $this->lasthash = sha1('');
-        } else {
-            $data = base64_decode($record->sessdata);
-            $this->lasthash = sha1($record->sessdata);
-        }
-
-        unset($record->sessdata); // conserve memory
-        $this->record = $record;
-
-        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 $sid
-     * @param string $session_data
-     * @return bool success
-     */
-    public function handler_write($sid, $session_data) {
-        global $USER;
-
-        // TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
-
-        if ($this->failed) {
-            // do not write anything back - we failed to start the session properly
-            return false;
-        }
-
-        $userid = 0;
-        if (!empty($USER->realuser)) {
-            $userid = $USER->realuser;
-        } else if (!empty($USER->id)) {
-            $userid = $USER->id;
-        }
-
-        if (isset($this->record->id)) {
-            $data = base64_encode($session_data);  // There might be some binary mess :-(
-
-            // Skip db update if nothing changed,
-            // do not update the timemodified each second.
-            $hash = sha1($data);
-            if ($this->lasthash === $hash
-                and $this->record->userid == $userid
-                and (time() - $this->record->timemodified < 20)
-                and $this->record->lastip == getremoteaddr()
-            ) {
-                // No need to update anything!
-                return true;
-            }
-
-            $this->record->sessdata     = $data;
-            $this->record->userid       = $userid;
-            $this->record->timemodified = time();
-            $this->record->lastip       = getremoteaddr();
-
-            try {
-                $this->database->update_record_raw('sessions', $this->record);
-                $this->lasthash = $hash;
-            } catch (dml_exception $ex) {
-                if ($this->database->get_dbfamily() === 'mysql') {
-                    try {
-                        $this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
-                    } catch (Exception $ignored) {
-                    }
-                    error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
-                } else {
-                    error_log('Can not write database session');
-                }
-                return false;
-            } catch (Exception $ex) {
-                error_log('Can not write database session');
-                return false;
-            }
-
-        } else {
-            // fresh new session
-            try {
-                $record = new stdClass();
-                $record->state        = 0;
-                $record->sid          = $sid;
-                $record->sessdata     = base64_encode($session_data); // there might be some binary mess :-(
-                $record->userid       = $userid;
-                $record->timecreated  = $record->timemodified = time();
-                $record->firstip      = $record->lastip = getremoteaddr();
-                $record->id           = $this->database->insert_record_raw('sessions', $record);
-
-                $this->record = $this->database->get_record('sessions', array('id'=>$record->id));
-                $this->lasthash = sha1($record->sessdata);
-
-                $this->database->get_session_lock($this->record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
-            } catch (Exception $ex) {
-                // this should not happen
-                error_log('Can not write new database session or acquire session lock');
-                $this->failed = true;
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Destroy session handler
-     *
-     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
-     *
-     * @param string $sid
-     * @return bool success
-     */
-    public function handler_destroy($sid) {
-        session_kill($sid);
-
-        if (isset($this->record->id) and $this->record->sid === $sid) {
-            try {
-                $this->database->release_session_lock($this->record->id);
-            } catch (Exception $ex) {
-                // ignore problems
-            }
-            $this->record = null;
-        }
-
-        $this->lasthash = null;
-
-        return true;
-    }
-
-    /**
-     * GC session handler
-     *
-     * {@see http://php.net/manual/en/function.session-set-save-handler.php}
-     *
-     * @param int $ignored_maxlifetime moodle uses special timeout rules
-     * @return bool success
-     */
-    public function handler_gc($ignored_maxlifetime) {
-        session_gc();
-        return true;
-    }
-}
-
-
-/**
- * returns true if legacy session used.
- * @return bool true if legacy(==file) based session used
- */
-function session_is_legacy() {
-    global $CFG, $DB;
-    return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
-}
-
-/**
- * Terminates all sessions, auth hooks are not executed.
- * Useful in upgrade scripts.
- */
-function session_kill_all() {
-    global $CFG, $DB;
-
-    // always check db table - custom session classes use sessions table
-    try {
-        $DB->delete_records('sessions');
-    } catch (dml_exception $ignored) {
-        // do not show any warnings - might be during upgrade/installation
-    }
-
-    if (session_is_legacy()) {
-        $sessiondir = "$CFG->dataroot/sessions";
-        if (is_dir($sessiondir)) {
-            foreach (glob("$sessiondir/sess_*") as $filename) {
-                @unlink($filename);
-            }
-        }
-    }
-}
-
-/**
- * Mark session as accessed, prevents timeouts.
- * @param string $sid
- */
-function session_touch($sid) {
-    global $CFG, $DB;
-
-    // always check db table - custom session classes use sessions table
-    try {
-        $sql = "UPDATE {sessions} SET timemodified=? WHERE sid=?";
-        $params = array(time(), $sid);
-        $DB->execute($sql, $params);
-    } catch (dml_exception $ignored) {
-        // do not show any warnings - might be during upgrade/installation
-    }
-
-    if (session_is_legacy()) {
-        $sid = clean_param($sid, PARAM_FILE);
-        $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
-        if (file_exists($sessionfile)) {
-            // if the file is locked it means that it will be updated anyway
-            @touch($sessionfile);
-        }
-    }
-}
-
-/**
- * Terminates one sessions, auth hooks are not executed.
- *
- * @param string $sid session id
- */
-function session_kill($sid) {
-    global $CFG, $DB;
-
-    // always check db table - custom session classes use sessions table
-    try {
-        $DB->delete_records('sessions', array('sid'=>$sid));
-    } catch (dml_exception $ignored) {
-        // do not show any warnings - might be during upgrade/installation
-    }
-
-    if (session_is_legacy()) {
-        $sid = clean_param($sid, PARAM_FILE);
-        $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
-        if (file_exists($sessionfile)) {
-            @unlink($sessionfile);
-        }
-    }
-}
-
-/**
- * Terminates all sessions of one user, auth hooks are not executed.
- * NOTE: This can not work for file based sessions!
- *
- * @param int $userid user id
- */
-function session_kill_user($userid) {
-    global $CFG, $DB;
-
-    // always check db table - custom session classes use sessions table
-    try {
-        $DB->delete_records('sessions', array('userid'=>$userid));
-    } catch (dml_exception $ignored) {
-        // do not show any warnings - might be during upgrade/installation
-    }
-
-    if (session_is_legacy()) {
-        // log error?
-    }
-}
-
-/**
- * Session garbage collection
- * - verify timeout for all users
- * - kill sessions of all deleted users
- * - kill sessions of users with disabled plugins or 'nologin' plugin
- *
- * NOTE: this can not work when legacy file sessions used!
- */
-function session_gc() {
-    global $CFG, $DB;
-
-    $maxlifetime = $CFG->sessiontimeout;
-
-    try {
-        /// kill all sessions of deleted users
-        $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0)");
-
-        /// kill sessions of users with disabled plugins
-        $auth_sequence = get_enabled_auth_plugins(true);
-        $auth_sequence = array_flip($auth_sequence);
-        unset($auth_sequence['nologin']); // no login allowed
-        $auth_sequence = array_flip($auth_sequence);
-        $notplugins = null;
-        list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
-        $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params);
-
-        /// now get a list of time-out candidates
-        $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 + ? < ? AND u.id <> ?";
-        $params = array($maxlifetime, time(), $CFG->siteguest);
-
-        $authplugins = array();
-        foreach($auth_sequence as $authname) {
-            $authplugins[$authname] = get_auth_plugin($authname);
-        }
-        $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;
-                }
-            }
-            $DB->delete_records('sessions', array('sid'=>$user->sid));
-        }
-        $rs->close();
-
-        // Extending the timeout period for guest sessions as they are renewed.
-        $purgebefore = time() - $maxlifetime;
-        $purgebeforeguests = time() - ($maxlifetime * 5);
-
-        // delete expired sessions for guest user account
-        $DB->delete_records_select('sessions', 'userid = ? AND timemodified < ?', array($CFG->siteguest, $purgebeforeguests));
-        // delete expired sessions for userid = 0 (not logged in)
-        $DB->delete_records_select('sessions', 'userid = 0 AND timemodified < ?', array($purgebefore));
-    } catch (dml_exception $ex) {
-        error_log('Error gc-ing sessions');
-    }
-}
 
 /**
  * Makes sure that $USER->sesskey exists, if $USER itself exists. It sets a new sesskey
@@ -1110,95 +147,6 @@ function get_moodle_cookie() {
     }
 }
 
-
-/**
- * Setup $USER object - called during login, loginas, etc.
- *
- * Call sync_user_enrolments() manually after log-in, or log-in-as.
- *
- * @param stdClass $user full user record object
- * @return void
- */
-function session_set_user($user) {
-    $_SESSION['USER'] = $user;
-    unset($_SESSION['USER']->description); // conserve memory
-    if (isset($_SESSION['USER']->lang)) {
-        // Make sure it is a valid lang pack name.
-        $_SESSION['USER']->lang = clean_param($_SESSION['USER']->lang, PARAM_LANG);
-    }
-    sesskey(); // init session key
-
-    if (PHPUNIT_TEST || defined('BEHAT_TEST')) {
-        // phpunit tests use reversed reference
-        global $USER;
-        $USER = $_SESSION['USER'];
-        $_SESSION['USER'] =& $USER;
-    }
-}
-
-/**
- * Is current $USER logged-in-as somebody else?
- * @return bool
- */
-function session_is_loggedinas() {
-    return !empty($_SESSION['USER']->realuser);
-}
-
-/**
- * Returns the $USER object ignoring current login-as session
- * @return stdClass user object
- */
-function session_get_realuser() {
-    if (session_is_loggedinas()) {
-        return $_SESSION['REALUSER'];
-    } else {
-        return $_SESSION['USER'];
-    }
-}
-
-/**
- * Login as another user - no security checks here.
- * @param int $userid
- * @param stdClass $context
- * @return void
- */
-function session_loginas($userid, $context) {
-    global $USER;
-
-    if (session_is_loggedinas()) {
-        return;
-    }
-
-    // Switch to fresh new $SESSION.
-    $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
-    $_SESSION['SESSION']     = new stdClass();
-
-    /// Create the new $USER object with all details and reload needed capabilities
-    $_SESSION['REALUSER'] = $_SESSION['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);
-
-    // 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.
-    session_set_user($user);
-    $event->trigger();
-}
-
 /**
  * Sets up current user and course environment (lang, etc.) in cron.
  * Do not use outside of cron script!
@@ -1226,13 +174,13 @@ function cron_setup_user($user = NULL, $course = NULL) {
 
     if (!$user) {
         // cached default cron user (==modified admin for now)
-        session_set_user($cronuser);
+        \core\session\manager::set_user($cronuser);
         $_SESSION['SESSION'] = $cronsession;
 
     } else {
         // emulate real user session - needed for caps in cron
         if ($_SESSION['USER']->id != $user->id) {
-            session_set_user($user);
+            \core\session\manager::set_user($user);
             $_SESSION['SESSION'] = new stdClass();
         }
     }
index ecf4917..4c7600a 100644 (file)
@@ -728,7 +728,7 @@ if (!defined('SYSCONTEXTID')) {
 // Defining the site - aka frontpage course
 try {
     $SITE = get_site();
-} catch (dml_exception $e) {
+} catch (moodle_exception $e) {
     $SITE = null;
     if (empty($CFG->version)) {
         $SITE = new stdClass();
@@ -760,8 +760,11 @@ if (CLI_SCRIPT) {
     }
 }
 
-// start session and prepare global $SESSION, $USER
-session_get_instance();
+// Start session and prepare global $SESSION, $USER.
+if (empty($CFG->sessiontimeout)) {
+    $CFG->sessiontimeout = 7200;
+}
+\core\session\manager::start();
 $SESSION = &$_SESSION['SESSION'];
 $USER    = &$_SESSION['USER'];
 
@@ -848,7 +851,7 @@ if (!empty($CFG->debugvalidators) and !empty($CFG->guestloginbutton)) {
                 } else {
                     $user = guest_user();
                 }
-                session_set_user($user);
+                \core\session\manager::set_user($user);
             }
         }
     }
@@ -867,8 +870,8 @@ if ($USER && function_exists('apache_note')
         $apachelog_name = clean_filename($USER->firstname . " " .
                                          $USER->lastname);
     }
-    if (session_is_loggedinas()) {
-        $realuser = session_get_realuser();
+    if (\core\session\manager::is_loggedinas()) {
+        $realuser = \core\session\manager::get_realuser();
         $apachelog_username = clean_filename($realuser->username." as ".$apachelog_username);
         $apachelog_name = clean_filename($realuser->firstname." ".$realuser->lastname ." as ".$apachelog_name);
         $apachelog_userid = clean_filename($realuser->id." as ".$apachelog_userid);
index 906ead5..fcbad37 100644 (file)
@@ -1165,7 +1165,7 @@ function redirect_if_major_upgrade_required() {
     if (empty($CFG->version) or (float)$CFG->version < $lastmajordbchanges or
             during_initial_install() or !empty($CFG->adminsetuppending)) {
         try {
-            @session_get_instance()->terminate_current();
+            @\core\session\manager::terminate_current();
         } catch (Exception $e) {
             // Ignore any errors, redirect to upgrade anyway.
         }
index 851b141..2abcd82 100644 (file)
@@ -185,26 +185,6 @@ class core_authlib_testcase extends advanced_testcase {
         ini_set('error_log', $oldlog);
     }
 
-    public function test_user_loggedin_event() {
-        global $USER;
-        $this->resetAfterTest(true);
-        $this->setAdminUser();
-
-        $sink = $this->redirectEvents();
-        $user = clone($USER);
-        login_attempt_valid($user);
-        $events = $sink->get_events();
-        $sink->close();
-
-        $this->assertCount(1, $events);
-        $event = reset($events);
-        $this->assertInstanceOf('\core\event\user_loggedin', $event);
-        $this->assertEquals('user', $event->objecttable);
-        $this->assertEquals('2', $event->objectid);
-        $this->assertEquals(context_system::instance()->id, $event->contextid);
-        $this->assertEquals($user, $event->get_record_snapshot('user', 2));
-    }
-
     public function test_user_loggedin_event_exceptions() {
         try {
             $event = \core\event\user_loggedin::create(array('objectid' => 1));
index b0e17e3..08e64a2 100644 (file)
@@ -176,7 +176,7 @@ class behat_hooks extends behat_base {
 
         // Assing valid data to admin user (some generator-related code needs a valid user).
         $user = $DB->get_record('user', array('username' => 'admin'));
-        session_set_user($user);
+        \core\session\manager::set_user($user);
 
         // Reset the browser if specified in config.php.
         if (!empty($CFG->behat_restart_browser_after) && $this->running_javascript()) {
index 2a70acc..4ef9da6 100644 (file)
@@ -2422,6 +2422,30 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
     }
 
+    public function test_complete_user_login() {
+        global $USER;
+
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser(0);
+
+        $sink = $this->redirectEvents();
+        @complete_user_login($user); // Hide session header errors.
+        $this->assertEquals($user->id, $USER->id);
+        $events = $sink->get_events();
+        $sink->close();
+
+        $this->assertCount(2, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\user_updated', $event);
+        $event = $events[1];
+        $this->assertInstanceOf('\core\event\user_loggedin', $event);
+        $this->assertEquals('user', $event->objecttable);
+        $this->assertEquals($user->id, $event->objectid);
+        $this->assertEquals(context_system::instance()->id, $event->contextid);
+        $this->assertEquals($user, $event->get_record_snapshot('user', $user->id));
+    }
+
     /**
      * Test require_logout.
      */
@@ -2429,7 +2453,6 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->resetAfterTest();
         $user = $this->getDataGenerator()->create_user();
         $this->setUser($user);
-        $course = $this->getDataGenerator()->create_course();
 
         $this->assertTrue(isloggedin());
 
diff --git a/lib/tests/session_manager_test.php b/lib/tests/session_manager_test.php
new file mode 100644 (file)
index 0000000..d964b9c
--- /dev/null
@@ -0,0 +1,374 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for session manager class.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for session manager class.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2013 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_session_manager_testcase extends advanced_testcase {
+    public function test_start() {
+        $this->resetAfterTest();
+        // Session must be started only once...
+        \core\session\manager::start();
+        $this->assertDebuggingCalled('Session was already started!', DEBUG_DEVELOPER);
+    }
+
+    public function test_set_user() {
+        global $USER;
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser(0);
+        $this->assertEquals(0, $USER->id);
+
+        \core\session\manager::set_user($user);
+        $this->assertEquals($user->id, $USER->id);
+    }
+
+    public function test_login_user() {
+        global $USER;
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser(0);
+        $this->assertEquals(0, $USER->id);
+
+        @\core\session\manager::login_user($user); // Ignore header error messages.
+        $this->assertEquals($user->id, $USER->id);
+    }
+
+    public function test_terminate_current() {
+        global $USER;
+        $this->resetAfterTest();
+
+        // This can not be tested much without real session...
+        $this->setAdminUser();
+        \core\session\manager::terminate_current();
+        $this->assertEquals(0, $USER->id);
+    }
+
+    public function test_write_close() {
+        global $USER;
+        $this->resetAfterTest();
+
+        // Just make sure no errors and $USER->id is kept
+        $this->setAdminUser();
+        $userid = $USER->id;
+        \core\session\manager::write_close();
+        $this->assertSame($userid, $USER->id);
+    }
+
+    public function test_session_exists() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // The file handler is used by default, so let's fake the data somehow.
+        $sid = md5('hokus');
+        mkdir("$CFG->dataroot/sessions/", $CFG->directorypermissions, true);
+        touch("$CFG->dataroot/sessions/sess_$sid");
+
+        $this->assertTrue(\core\session\manager::session_exists($sid));
+    }
+
+    public function test_touch_session() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $sid = md5('hokus');
+        $record = new \stdClass();
+        $record->state        = 0;
+        $record->sid          = $sid;
+        $record->sessdata     = null;
+        $record->userid       = 2;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 30;
+        $record->firstip      = $record->lastip = '10.0.0.1';
+        $record->id = $DB->insert_record('sessions', $record);
+
+        $now = time();
+        \core\session\manager::touch_session($sid);
+        $updated = $DB->get_field('sessions', 'timemodified', array('id'=>$record->id));
+
+        $this->assertGreaterThanOrEqual($now, $updated);
+        $this->assertLessThanOrEqual(time(), $updated);
+    }
+
+    public function test_kill_session() {
+        global $DB, $USER;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $userid = $USER->id;
+
+        $sid = md5('hokus');
+        $record = new \stdClass();
+        $record->state        = 0;
+        $record->sid          = $sid;
+        $record->sessdata     = null;
+        $record->userid       = $userid;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 30;
+        $record->firstip      = $record->lastip = '10.0.0.1';
+        $DB->insert_record('sessions', $record);
+
+        $record->userid       = 0;
+        $record->sid          = md5('pokus');
+        $DB->insert_record('sessions', $record);
+
+        $this->assertEquals(2, $DB->count_records('sessions'));
+
+        \core\session\manager::kill_session($sid);
+
+        $this->assertEquals(1, $DB->count_records('sessions'));
+        $this->assertFalse($DB->record_exists('sessions', array('sid'=>$sid)));
+
+        $this->assertSame($userid, $USER->id);
+    }
+
+    public function test_kill_user_sessions() {
+        global $DB, $USER;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $userid = $USER->id;
+
+        $sid = md5('hokus');
+        $record = new \stdClass();
+        $record->state        = 0;
+        $record->sid          = $sid;
+        $record->sessdata     = null;
+        $record->userid       = $userid;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 30;
+        $record->firstip      = $record->lastip = '10.0.0.1';
+        $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus2');
+        $DB->insert_record('sessions', $record);
+
+        $record->userid       = 0;
+        $record->sid          = md5('pokus');
+        $DB->insert_record('sessions', $record);
+
+        $this->assertEquals(3, $DB->count_records('sessions'));
+
+        \core\session\manager::kill_user_sessions($userid);
+
+        $this->assertEquals(1, $DB->count_records('sessions'));
+        $this->assertFalse($DB->record_exists('sessions', array('userid'=>$userid)));
+    }
+
+    public function test_kill_all_sessions() {
+        global $DB, $USER;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $userid = $USER->id;
+
+        $sid = md5('hokus');
+        $record = new \stdClass();
+        $record->state        = 0;
+        $record->sid          = $sid;
+        $record->sessdata     = null;
+        $record->userid       = $userid;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 30;
+        $record->firstip      = $record->lastip = '10.0.0.1';
+        $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus2');
+        $DB->insert_record('sessions', $record);
+
+        $record->userid       = 0;
+        $record->sid          = md5('pokus');
+        $DB->insert_record('sessions', $record);
+
+        $this->assertEquals(3, $DB->count_records('sessions'));
+
+        \core\session\manager::kill_all_sessions();
+
+        $this->assertEquals(0, $DB->count_records('sessions'));
+        $this->assertSame(0, $USER->id);
+    }
+
+    public function test_gc() {
+        global $CFG, $DB, $USER;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $adminid = $USER->id;
+        $this->setGuestUser();
+        $guestid = $USER->id;
+        $this->setUser(0);
+
+        $CFG->sessiontimeout = 60*10;
+
+        $record = new \stdClass();
+        $record->state        = 0;
+        $record->sid          = md5('hokus1');
+        $record->sessdata     = null;
+        $record->userid       = $adminid;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 30;
+        $record->firstip      = $record->lastip = '10.0.0.1';
+        $r1 = $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus2');
+        $record->userid       = $adminid;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 60*20;
+        $r2 = $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus3');
+        $record->userid       = $guestid;
+        $record->timecreated  = time() - 60*60*60;
+        $record->timemodified = time() - 60*20;
+        $r3 = $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus4');
+        $record->userid       = $guestid;
+        $record->timecreated  = time() - 60*60*60;
+        $record->timemodified = time() - 60*10*5 - 60;
+        $r4 = $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus5');
+        $record->userid       = 0;
+        $record->timecreated  = time() - 60*5;
+        $record->timemodified = time() - 60*5;
+        $r5 = $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus6');
+        $record->userid       = 0;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 60*10 -10;
+        $r6 = $DB->insert_record('sessions', $record);
+
+        $record->sid          = md5('hokus7');
+        $record->userid       = 0;
+        $record->timecreated  = time() - 60*60;
+        $record->timemodified = time() - 60*9;
+        $r7 = $DB->insert_record('sessions', $record);
+
+        \core\session\manager::gc();
+
+        $this->assertTrue($DB->record_exists('sessions', array('id'=>$r1)));
+        $this->assertFalse($DB->record_exists('sessions', array('id'=>$r2)));
+        $this->assertTrue($DB->record_exists('sessions', array('id'=>$r3)));
+        $this->assertFalse($DB->record_exists('sessions', array('id'=>$r4)));
+        $this->assertFalse($DB->record_exists('sessions', array('id'=>$r5)));
+        $this->assertFalse($DB->record_exists('sessions', array('id'=>$r6)));
+        $this->assertTrue($DB->record_exists('sessions', array('id'=>$r7)));
+    }
+
+    /**
+     * Test loginas.
+     * @copyright  2103 Rajesh Taneja <rajesh@moodle.com>
+     */
+    public function test_loginas() {
+        global $USER;
+        $this->resetAfterTest();
+
+        // Set current user as Admin user and save it for later use.
+        $this->setAdminUser();
+        $adminuser = $USER;
+
+        // Create a new user and try admin loginas this user.
+        $user = $this->getDataGenerator()->create_user();
+        \core\session\manager::loginas($user->id, context_system::instance());
+
+        $this->assertSame($user->id, $USER->id);
+        $this->assertSame(context_system::instance(), $USER->loginascontext);
+        $this->assertSame($adminuser->id, $USER->realuser);
+
+        // Set user as current user and login as admin user in course context.
+        $this->setUser($user);
+        $this->assertNotEquals($adminuser->id, $USER->id);
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        // Catch event triggered.
+        $sink = $this->redirectEvents();
+        \core\session\manager::loginas($adminuser->id, $coursecontext);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        $this->assertSame($adminuser->id, $USER->id);
+        $this->assertSame($coursecontext, $USER->loginascontext);
+        $this->assertSame($user->id, $USER->realuser);
+
+        // Test event captured has proper information.
+        $this->assertInstanceOf('\core\event\user_loggedinas', $event);
+        $this->assertSame($user->id, $event->objectid);
+        $this->assertSame($adminuser->id, $event->relateduserid);
+        $this->assertSame($course->id, $event->courseid);
+        $this->assertEquals($coursecontext, $event->get_context());
+        $oldfullname = fullname($user, true);
+        $newfullname = fullname($adminuser, true);
+        $expectedlogdata = array($course->id, "course", "loginas", "../user/view.php?id=$course->id&amp;user=$user->id", "$oldfullname -> $newfullname");
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+    }
+
+    public function test_is_loggedinas() {
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $this->assertFalse(\core\session\manager::is_loggedinas());
+
+        $this->setUser($user1);
+        \core\session\manager::loginas($user2->id, context_system::instance());
+
+        $this->assertTrue(\core\session\manager::is_loggedinas());
+    }
+
+    public function test_get_realuser() {
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+        \core\session\manager::loginas($user2->id, context_system::instance());
+
+        $real = \core\session\manager::get_realuser();
+
+        unset($real->password);
+        unset($real->description);
+        unset($real->sesskey);
+        unset($user1->password);
+        unset($user1->description);
+        unset($user1->sesskey);
+
+        $this->assertEquals($real, $user1);
+    }
+}
diff --git a/lib/tests/sessionlib_test.php b/lib/tests/sessionlib_test.php
deleted file mode 100644 (file)
index 5f3b032..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Unit tests for (some of) ../sessionlib.php.
- *
- * @package    core_session
- * @category   phpunit
- * @copyright  2103 Rajesh Taneja <rajesh@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/sessionlib.php');
-
-/**
- * Unit tests for (some of) ../sessionlib.php.
- *
- * @package    core_session
- * @category   phpunit
- * @copyright  2103 Rajesh Taneja <rajesh@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class core_sessionlib_testcase extends advanced_testcase {
-
-    /**
-     * Test session_loginas.
-     */
-    public function test_session_loginas() {
-        global $USER;
-        $this->resetAfterTest();
-
-        // Set current user as Admin user and save it for later use.
-        $this->setAdminUser();
-        $adminuser = $USER;
-
-        // Create a new user and try admin loginas this user.
-        $user = $this->getDataGenerator()->create_user();
-        session_loginas($user->id, context_system::instance());
-
-        $this->assertSame($user->id, $USER->id);
-        $this->assertSame(context_system::instance(), $USER->loginascontext);
-        $this->assertSame($adminuser->id, $USER->realuser);
-
-        // Set user as current user and login as admin user in course context.
-        $this->setUser($user);
-        $this->assertNotEquals($adminuser->id, $USER->id);
-        $course = $this->getDataGenerator()->create_course();
-        $coursecontext = context_course::instance($course->id);
-
-        // Catch event triggred.
-        $sink = $this->redirectEvents();
-        session_loginas($adminuser->id, $coursecontext);
-        $events = $sink->get_events();
-        $sink->close();
-        $event = array_pop($events);
-
-        $this->assertSame($adminuser->id, $USER->id);
-        $this->assertSame($coursecontext, $USER->loginascontext);
-        $this->assertSame($user->id, $USER->realuser);
-
-        // Test event captured has proper information.
-        $this->assertInstanceOf('\core\event\user_loggedinas', $event);
-        $this->assertSame($user->id, $event->objectid);
-        $this->assertSame($adminuser->id, $event->relateduserid);
-        $this->assertSame($course->id, $event->courseid);
-        $this->assertEquals($coursecontext, $event->get_context());
-        $oldfullname = fullname($user, true);
-        $newfullname = fullname($adminuser, true);
-        $expectedlogdata = array($course->id, "course", "loginas", "../user/view.php?id=$course->id&amp;user=$user->id", "$oldfullname -> $newfullname");
-        $this->assertEventLegacyLogData($expectedlogdata, $event);
-    }
-}
index ae66726..62d2037 100644 (file)
@@ -110,6 +110,18 @@ Misc:
     * course_modinfo::build_section_cache() -> (no replacement)
     * generate_email_supportuser()          -> core_user::get_support_user()
 
+Sessions:
+    * session_get_instance()->xxx()         -> \core\session\manager::xxx()
+    * session_kill_all()                    -> \core\session\manager::kill_all_sessions()
+    * session_touch()                       -> \core\session\manager::touch_session()
+    * session_kill()                        -> \core\session\manager::kill_session()
+    * session_kill_user()                   -> \core\session\manager::kill_user_sessions()
+    * session_gc()                          -> \core\session\manager::gc()
+    * session_set_user()                    -> \core\session\manager::set_user()
+    * session_is_loggedinas()               -> \core\session\manager::is_loggedinas()
+    * session_get_realuser()                -> \core\session\manager::get_realuser()
+    * session_loginas()                     -> \core\session\manager::loginas()
+
 User-agent related functions:
     * check_browser_operating_system()      -> core_useragent::check_browser_operating_system()
     * check_browser_version()               -> core_useragent::check_browser_version()
index fdde141..20e9997 100644 (file)
@@ -2588,12 +2588,11 @@ function redirect($url, $message='', $delay=-1) {
         }
     }
 
-    if ($delay == 0 && !$debugdisableredirect && !headers_sent()) {
-        // Workaround for IIS bug http://support.microsoft.com/kb/q176113/.
-        if (session_id()) {
-            session_get_instance()->write_close();
-        }
+    // Make sure the session is closed properly, this prevents problems in IIS
+    // and also some potential PHP shutdown issues.
+    \core\session\manager::write_close();
 
+    if ($delay == 0 && !$debugdisableredirect && !headers_sent()) {
         // 302 might not work for POST requests, 303 is ignored by obsolete clients.
         @header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other');
         @header('Location: '.$url);
index 73f0e8e..88a7cf5 100644 (file)
@@ -73,7 +73,7 @@ if (!get_user_preferences('auth_forcepasswordchange', false)) {
 }
 
 // do not allow "Logged in as" users to change any passwords
-if (session_is_loggedinas()) {
+if (\core\session\manager::is_loggedinas()) {
     print_error('cannotcallscript');
 }
 
index 412b247..9fd88b0 100644 (file)
@@ -68,7 +68,7 @@ if (!empty($user)) {
     enrol_check_plugins($user);
 
     // setup user session to check capability
-    session_set_user($user);
+    \core\session\manager::set_user($user);
 
     //check if the service exists and is enabled
     $service = $DB->get_record('external_services', array('shortname' => $serviceshortname, 'enabled' => 1));
@@ -116,8 +116,7 @@ if (!empty($user)) {
         $unsettoken = false;
         //if sid is set then there must be a valid associated session no matter the token type
         if (!empty($token->sid)) {
-            $session = session_get_instance();
-            if (!$session->session_exists($token->sid)){
+            if (!\core\session\manager::session_exists($token->sid)){
                 //this token will never be valid anymore, delete it
                 $DB->delete_records('external_tokens', array('sid'=>$token->sid));
                 $unsettoken = true;
index 63c4367..8888caa 100644 (file)
@@ -420,7 +420,7 @@ class assignment_online extends assignment_base {
             send_file_not_found();
         }
 
-        session_get_instance()->write_close(); // unlock session during fileserving
+        \core\session\manager::write_close(); // Unlock session during file serving.
 
         send_stored_file($file, 60*60, 0, true, $options);
     }
index d9e6b37..8de18fc 100644 (file)
@@ -68,7 +68,7 @@ case 'init':
     break;
 
 case 'chat':
-    session_get_instance()->write_close();
+    \core\session\manager::write_close();
     chat_delete_old_users();
     $chat_message = clean_text($chat_message, FORMAT_MOODLE);
 
index 6b842d2..0573fcf 100644 (file)
@@ -30,7 +30,7 @@ if (isguestuser()) {
     print_error('noguests');
 }
 
-session_get_instance()->write_close();
+\core\session\manager::write_close();
 
 /// Delete old users now
 
index 38bb483..5f62bc1 100644 (file)
@@ -320,7 +320,7 @@ class quiz_overview_report extends quiz_attempts_report {
      * Unlock the session and allow the regrading process to run in the background.
      */
     protected function unlock_session() {
-        session_get_instance()->write_close();
+        \core\session\manager::write_close();
         ignore_user_abort(true);
     }
 
index 6468d04..ad94639 100644 (file)
@@ -135,7 +135,7 @@ $adminediting = optional_param('adminedit', -1, PARAM_BOOL);
 if ($PAGE->user_allowed_editing() && $adminediting != -1) {
     $USER->editing = $adminediting;
 }
-session_get_instance()->write_close();
+\core\session\manager::write_close();
 
 if (!empty($chooselog)) {
     $userinfo = get_string('allparticipants');
index 90e3eb9..7bb84d8 100644 (file)
@@ -46,7 +46,7 @@ require_capability('report/loglive:view', $context);
 $strlivelogs = get_string('livelogs', 'report_loglive');
 
 if ($inpopup) {
-    session_get_instance()->write_close();
+    \core\session\manager::write_close();
 
     $date = time() - 3600;
 
index 276c0e9..4231205 100644 (file)
@@ -743,7 +743,7 @@ abstract class repository implements cacheable_object {
         $repocontext = context::instance_by_id($this->instance->contextid);
 
         // Prevent access to private repositories when logged in as.
-        if ($can && session_is_loggedinas()) {
+        if ($can && \core\session\manager::is_loggedinas()) {
             if ($this->contains_private_data() || $repocontext->contextlevel == CONTEXT_USER) {
                 $can = false;
             }
index 3d41ab0..fefb6be 100644 (file)
@@ -436,7 +436,7 @@ class core_repositorylib_testcase extends advanced_testcase {
         $userrepo = repository::get_repository_by_id($user1repoid, $syscontext);
 
         $this->setAdminUser();
-        session_loginas($user1->id, $syscontext);
+        \core\session\manager::loginas($user1->id, $syscontext);
 
         // Logged in as, I cannot view a user instance.
         $caughtexception = false;
index 6d7dbc0..cdd6f89 100644 (file)
@@ -128,7 +128,7 @@ $user = get_complete_user_data('id', $userid);
 // let enrol plugins deal with new enrolments if necessary
 enrol_check_plugins($user);
 
-session_set_user($user); //for login and capability checks
+\core\session\manager::set_user($user); //for login and capability checks
 
 try {
     $autologinguest = true;
index 2969e97..fa81269 100644 (file)
@@ -307,8 +307,8 @@ class theme_mymobile_core_renderer extends core_renderer {
 
         $course = $this->page->course;
 
-        if (session_is_loggedinas()) {
-            $realuser = session_get_realuser();
+        if (\core\session\manager::is_loggedinas()) {
+            $realuser = \core\session\manager::get_realuser();
             $fullname = fullname($realuser, true);
             $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\">$fullname</a>] ";
         } else {
@@ -386,8 +386,8 @@ class theme_mymobile_core_renderer extends core_renderer {
         $loginpage = ((string)$this->page->url === get_login_url());
         $course = $this->page->course;
 
-        if (session_is_loggedinas()) {
-            $realuser = session_get_realuser();
+        if (\core\session\manager::is_loggedinas()) {
+            $realuser = \core\session\manager::get_realuser();
             $fullname = fullname($realuser, true);
             $realuserinfo = ' [<a href="'.$CFG->wwwroot.'/course/loginas.php?id=$course->id&amp;sesskey='.sesskey().'">$fullname</a>] ';
         } else {
@@ -628,7 +628,7 @@ class theme_mymobile_core_renderer extends core_renderer {
     public function header() {
         global $USER, $CFG;
 
-        if (session_is_loggedinas()) {
+        if (\core\session\manager::is_loggedinas()) {
             $this->page->add_body_class('userloggedinas');
         }
 
index 9728bbd..924ebb1 100644 (file)
@@ -200,7 +200,7 @@ if ($usernew = $userform->get_data()) {
 
         // force logout if user just suspended
         if (isset($usernew->suspended) and $usernew->suspended and !$user->suspended) {
-            session_kill_user($user->id);
+            \core\session\manager::kill_user_sessions($user->id);
         }
     }
 
@@ -255,7 +255,7 @@ if ($usernew = $userform->get_data()) {
             redirect("$CFG->wwwroot/user/view.php?id=$USER->id&course=$course->id");
         }
     } else {
-        session_gc(); // remove stale sessions
+        \core\session\manager::gc(); // Remove stale sessions.
         redirect("$CFG->wwwroot/$CFG->admin/user.php");
     }
     //never reached
index 8130981..c6029b1 100644 (file)
                         $links[] = html_writer::link(new moodle_url('/course/user.php?id='. $course->id .'&user='. $user->id), get_string('activity'));
                     }
 
-                    if ($USER->id != $user->id && !session_is_loggedinas() && has_capability('moodle/user:loginas', $context) && !is_siteadmin($user->id)) {
+                    if ($USER->id != $user->id && !\core\session\manager::is_loggedinas() && has_capability('moodle/user:loginas', $context) && !is_siteadmin($user->id)) {
                         $links[] = html_writer::link(new moodle_url('/course/loginas.php?id='. $course->id .'&user='. $user->id .'&sesskey='. sesskey()), get_string('loginas'));
                     }
 
index 8e1898f..1723d72 100644 (file)
@@ -90,12 +90,11 @@ class webservice {
         enrol_check_plugins($user);
 
         // setup user session to check capability
-        session_set_user($user);
+        \core\session\manager::set_user($user);
 
         //assumes that if sid is set then there must be a valid associated session no matter the token type
         if ($token->sid) {
-            $session = session_get_instance();
-            if (!$session->session_exists($token->sid)) {
+            if (!\core\session\manager::session_exists($token->sid)) {
                 $DB->delete_records('external_tokens', array('sid' => $token->sid));
                 throw new webservice_access_exception('Invalid session based token - session not found or expired');
             }
@@ -905,7 +904,7 @@ abstract class webservice_server implements webservice_server_interface {
 
         // now fake user login, the session is completely empty too
         enrol_check_plugins($user);
-        session_set_user($user);
+        \core\session\manager::set_user($user);
         $this->userid = $user->id;
 
         if ($this->authmethod != WEBSERVICE_AUTHMETHOD_SESSION_TOKEN && !has_capability("webservice/$this->wsname:use", $this->restricted_context)) {
@@ -936,8 +935,7 @@ abstract class webservice_server implements webservice_server_interface {
         }
 
         if ($token->sid){//assumes that if sid is set then there must be a valid associated session no matter the token type
-            $session = session_get_instance();
-            if (!$session->session_exists($token->sid)){
+            if (!\core\session\manager::session_exists($token->sid)){
                 $DB->delete_records('external_tokens', array('sid'=>$token->sid));
                 throw new webservice_access_exception('Invalid session based token - session not found or expired');
             }