Commit 2e00d01d authored by Petr Skoda's avatar Petr Skoda
Browse files

MDL-46099 session: fix use of references for session globals

This reverses the references used for global $USER and $SESSION,
the reason is that PHP does not allow references to references.
$USER is a reference to $GLOBALS['USER'] which means we cannot
put any references to it. Solution is to store the current user and session
objects in $GLOBALS['USER'] and $GLOBALS['SESSIOn'] are reference
them in $_SESSION.

This patch makes the session code behave the same way in CLI,
phpunit and normal web requests - this allows use to finally
unit test most aspects of the session code in Moodle.
parent 7a4832ec
......@@ -201,17 +201,9 @@ if (defined('COMPONENT_CLASSLOADER')) {
require($CFG->dirroot.'/version.php');
$CFG->target_release = $release;
$_SESSION = array();
$_SESSION['SESSION'] = new stdClass();
$_SESSION['SESSION']->lang = $CFG->lang;
$_SESSION['USER'] = new stdClass();
$_SESSION['USER']->id = 0;
$_SESSION['USER']->mnethostid = 1;
\core\session\manager::init_empty_session();
global $SESSION;
global $USER;
$SESSION = &$_SESSION['SESSION'];
$USER = &$_SESSION['USER'];
global $COURSE;
$COURSE = new stdClass();
......
......@@ -72,7 +72,7 @@ trait buffered_writer {
$entry['other'] = serialize($entry['other']);
$entry['origin'] = $PAGE->requestorigin;
$entry['ip'] = $PAGE->requestip;
$entry['realuserid'] = \core\session\manager::is_loggedinas() ? $_SESSION['USER']->realuser : null;
$entry['realuserid'] = \core\session\manager::is_loggedinas() ? $GLOBALS['USER']->realuser : null;
$this->buffer[] = $entry;
$this->count++;
......
......@@ -130,8 +130,7 @@ class logstore_database_store_testcase extends advanced_testcase {
array('context' => context_module::instance($module2->cmid), 'other' => array('sample' => 6, 'xx' => 9)));
$event2->trigger();
$_SESSION['SESSION'] = new \stdClass();
$this->setUser(0);
\core\session\manager::init_empty_session();
$this->assertFalse(\core\session\manager::is_loggedinas());
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
......
......@@ -95,8 +95,7 @@ class logstore_standard_store_testcase extends advanced_testcase {
$event2->trigger();
logstore_standard_restore::hack_executing(0);
$_SESSION['SESSION'] = new \stdClass();
$this->setUser(0);
\core\session\manager::init_empty_session();
$this->assertFalse(\core\session\manager::is_loggedinas());
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
......
......@@ -229,17 +229,9 @@ if (defined('COMPONENT_CLASSLOADER')) {
require('version.php');
$CFG->target_release = $release;
$_SESSION = array();
$_SESSION['SESSION'] = new stdClass();
$_SESSION['SESSION']->lang = $CFG->lang;
$_SESSION['USER'] = new stdClass();
$_SESSION['USER']->id = 0;
$_SESSION['USER']->mnethostid = 1;
\core\session\manager::init_empty_session();
global $SESSION;
global $USER;
$SESSION = &$_SESSION['SESSION'];
$USER = &$_SESSION['USER'];
global $COURSE;
$COURSE = new stdClass();
......
......@@ -79,6 +79,16 @@ class manager {
self::initialise_user_session($newsid);
self::check_security();
// Link global $USER and $SESSION,
// this is tricky because PHP does not allow references to references
// and global keyword uses internally once reference to the $GLOBALS array.
// The solution is to use the $GLOBALS['USER'] and $GLOBALS['$SESSION']
// as the main storage of data and put references to $_SESSION.
$GLOBALS['USER'] = $_SESSION['USER'];
$_SESSION['USER'] =& $GLOBALS['USER'];
$GLOBALS['SESSION'] = $_SESSION['SESSION'];
$_SESSION['SESSION'] =& $GLOBALS['SESSION'];
} catch (\Exception $ex) {
@session_write_close();
self::init_empty_session();
......@@ -139,31 +149,29 @@ class manager {
/**
* Empty current session, fill it with not-logged-in user info.
*
* This is intended for installation scripts, unit tests and other
* special areas. Do NOT use for logout and session termination
* in normal requests!
*/
protected static function init_empty_session() {
public 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;
$GLOBALS['SESSION'] = new \stdClass();
$GLOBALS['USER'] = new \stdClass();
$GLOBALS['USER']->id = 0;
if (isset($CFG->mnet_localhost_id)) {
$_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
$GLOBALS['USER']->mnethostid = $CFG->mnet_localhost_id;
} else {
// Not installed yet, the future host id will be most probably 1.
$_SESSION['USER']->mnethostid = 1;
$GLOBALS['USER']->mnethostid = 1;
}
if (PHPUNIT_TEST or defined('BEHAT_TEST')) {
// Phpunit tests and behat init use reversed reference,
// the reason is we can not point global to $_SESSION outside of global scope.
global $USER, $SESSION;
$USER = $_SESSION['USER'];
$SESSION = $_SESSION['SESSION'];
$_SESSION['USER'] =& $USER;
$_SESSION['SESSION'] =& $SESSION;
}
// Link global $USER and $SESSION.
$_SESSION = array();
$_SESSION['USER'] =& $GLOBALS['USER'];
$_SESSION['SESSION'] =& $GLOBALS['SESSION'];
}
/**
......@@ -249,9 +257,11 @@ class manager {
}
/**
* Initialise $USER and $SESSION objects, handles google access
* Initialise $_SESSION, handles google access
* and sets up not-logged-in user properly.
*
* WARNING: $USER and $SESSION are set up later, do not use them yet!
*
* @param bool $newsid is this a new session in first http request?
*/
protected static function initialise_user_session($newsid) {
......@@ -416,6 +426,8 @@ class manager {
/**
* Do various session security checks.
*
* WARNING: $USER and $SESSION are set up later, do not use them yet!
*/
protected static function check_security() {
global $CFG;
......@@ -489,7 +501,7 @@ class manager {
session_regenerate_id(true);
$DB->delete_records('sessions', array('sid'=>$sid));
self::init_empty_session();
self::add_session_record($_SESSION['USER']->id);
self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
session_write_close();
self::$sessionactive = false;
}
......@@ -590,22 +602,19 @@ class manager {
* @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)) {
$GLOBALS['USER'] = $user;
unset($GLOBALS['USER']->description); // Conserve memory.
unset($GLOBALS['USER']->password); // Improve security.
if (isset($GLOBALS['USER']->lang)) {
// Make sure it is a valid lang pack name.
$_SESSION['USER']->lang = clean_param($_SESSION['USER']->lang, PARAM_LANG);
$GLOBALS['USER']->lang = clean_param($GLOBALS['USER']->lang, PARAM_LANG);
}
sesskey(); // Init session key.
if (PHPUNIT_TEST or defined('BEHAT_TEST')) {
// Phpunit tests and behat init use reversed reference,
// the reason is we can not point global to $_SESSION outside of global scope.
global $USER;
$USER = $_SESSION['USER'];
$_SESSION['USER'] =& $USER;
}
// Relink session with global $USER just in case it got unlinked somehow.
$_SESSION['USER'] =& $GLOBALS['USER'];
// Init session key.
sesskey();
}
/**
......@@ -697,7 +706,7 @@ class manager {
* @return bool
*/
public static function is_loggedinas() {
return !empty($_SESSION['USER']->realuser);
return !empty($GLOBALS['USER']->realuser);
}
/**
......@@ -708,7 +717,7 @@ class manager {
if (self::is_loggedinas()) {
return $_SESSION['REALUSER'];
} else {
return $_SESSION['USER'];
return $GLOBALS['USER'];
}
}
......@@ -725,12 +734,14 @@ class manager {
return;
}
// Switch to fresh new $SESSION.
$_SESSION['REALSESSION'] = $_SESSION['SESSION'];
$_SESSION['SESSION'] = new \stdClass();
// Switch to fresh new $_SESSION.
$_SESSION = array();
$_SESSION['REALSESSION'] = clone($GLOBALS['SESSION']);
$GLOBALS['SESSION'] = new \stdClass();
$_SESSION['SESSION'] =& $GLOBALS['SESSION'];
// Create the new $USER object with all details and reload needed capabilities.
$_SESSION['REALUSER'] = $_SESSION['USER'];
$_SESSION['REALUSER'] = clone($GLOBALS['USER']);
$user = get_complete_user_data('id', $userid);
$user->realuser = $_SESSION['REALUSER']->id;
$user->loginascontext = $context;
......
......@@ -189,14 +189,9 @@ class phpunit_util extends testing_util {
$FULLME = null;
$ME = null;
$SCRIPT = null;
$SESSION = new stdClass();
$_SESSION['SESSION'] =& $SESSION;
// set fresh new not-logged-in user
$user = new stdClass();
$user->id = 0;
$user->mnethostid = $CFG->mnet_localhost_id;
\core\session\manager::set_user($user);
// Empty sessison and set fresh new not-logged-in user.
\core\session\manager::init_empty_session();
// reset all static caches
\core\event\manager::phpunit_reset();
......
......@@ -76,6 +76,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
$this->assertEquals(0, $USER->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$user = $DB->get_record('user', array('id'=>2));
$this->assertNotEmpty($user);
......@@ -83,26 +84,31 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
$this->assertEquals(2, $USER->id);
$this->assertEquals(2, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$USER->id = 3;
$this->assertEquals(3, $USER->id);
$this->assertEquals(3, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
\core\session\manager::set_user($user);
$this->assertEquals(2, $USER->id);
$this->assertEquals(2, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$USER = $DB->get_record('user', array('id'=>1));
$this->assertNotEmpty($USER);
$this->assertEquals(1, $USER->id);
$this->assertEquals(1, $_SESSION['USER']->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
$this->setUser(null);
$this->assertEquals(0, $USER->id);
$this->assertSame($_SESSION['USER'], $USER);
$this->assertSame($GLOBALS['USER'], $USER);
}
public function test_set_admin_user() {
......
......@@ -37,7 +37,10 @@ function sesskey() {
// note: do not use $USER because it may not be initialised yet
if (empty($_SESSION['USER']->sesskey)) {
if (!isset($_SESSION['USER'])) {
$_SESSION['USER'] = new stdClass;
// This should never happen,
// do not mess with session and globals here,
// let any checks fail instead!
return false;
}
$_SESSION['USER']->sesskey = random_string(10);
}
......@@ -151,16 +154,28 @@ function get_moodle_cookie() {
* Sets up current user and course environment (lang, etc.) in cron.
* Do not use outside of cron script!
*
* @param stdClass $user full user object, null means default cron user (admin)
* @param $course full course record, null means $SITE
* @param stdClass $user full user object, null means default cron user (admin),
* value 'reset' means reset internal static caches.
* @param stdClass $course full course record, null means $SITE
* @return void
*/
function cron_setup_user($user = NULL, $course = NULL) {
global $CFG, $SITE, $PAGE;
if (!CLI_SCRIPT) {
throw new coding_exception('Function cron_setup_user() cannot be used in normal requests!');
}
static $cronuser = NULL;
static $cronsession = NULL;
if ($user === 'reset') {
$cronuser = null;
$cronsession = null;
\core\session\manager::init_empty_session();
return;
}
if (empty($cronuser)) {
/// ignore admins timezone, language and locale - use site default instead!
$cronuser = get_admin();
......@@ -173,15 +188,16 @@ function cron_setup_user($user = NULL, $course = NULL) {
}
if (!$user) {
// cached default cron user (==modified admin for now)
// Cached default cron user (==modified admin for now).
\core\session\manager::init_empty_session();
\core\session\manager::set_user($cronuser);
$_SESSION['SESSION'] = $cronsession;
$GLOBALS['SESSION'] = $cronsession;
} else {
// emulate real user session - needed for caps in cron
if ($_SESSION['USER']->id != $user->id) {
// Emulate real user session - needed for caps in cron.
if ($GLOBALS['USER']->id != $user->id) {
\core\session\manager::init_empty_session();
\core\session\manager::set_user($user);
$_SESSION['SESSION'] = new stdClass();
}
}
......
......@@ -771,10 +771,6 @@ if (empty($CFG->sessiontimeout)) {
$CFG->sessiontimeout = 7200;
}
\core\session\manager::start();
if (!PHPUNIT_TEST and !defined('BEHAT_TEST')) {
$SESSION =& $_SESSION['SESSION'];
$USER =& $_SESSION['USER'];
}
// Initialise some variables that are supposed to be set in config.php only.
if (!isset($CFG->filelifetime)) {
......
......@@ -189,9 +189,7 @@ class behat_hooks extends behat_base {
}
// Reset $SESSION.
$_SESSION = array();
$SESSION = new stdClass();
$_SESSION['SESSION'] =& $SESSION;
\core\session\manager::init_empty_session();
behat_util::reset_database();
behat_util::reset_dataroot();
......
......@@ -41,38 +41,122 @@ class core_session_manager_testcase extends advanced_testcase {
$this->assertDebuggingCalled('Session was already started!', DEBUG_DEVELOPER);
}
public function test_init_empty_session() {
global $SESSION, $USER;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$SESSION->test = true;
$this->assertTrue($GLOBALS['SESSION']->test);
$this->assertTrue($_SESSION['SESSION']->test);
\core\session\manager::set_user($user);
$this->assertSame($user, $USER);
$this->assertSame($user, $GLOBALS['USER']);
$this->assertSame($user, $_SESSION['USER']);
\core\session\manager::init_empty_session();
$this->assertInstanceOf('stdClass', $SESSION);
$this->assertEmpty((array)$SESSION);
$this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
$this->assertSame($GLOBALS['SESSION'], $SESSION);
$this->assertInstanceOf('stdClass', $USER);
$this->assertEquals(array('id' => 0, 'mnethostid' => 1), (array)$USER, '', 0, 10, true);
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
// Now test how references work.
$GLOBALS['SESSION'] = new \stdClass();
$GLOBALS['SESSION']->test = true;
$this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
$this->assertSame($GLOBALS['SESSION'], $SESSION);
$SESSION = new \stdClass();
$SESSION->test2 = true;
$this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
$this->assertSame($GLOBALS['SESSION'], $SESSION);
$_SESSION['SESSION'] = new stdClass();
$_SESSION['SESSION']->test3 = true;
$this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
$this->assertSame($GLOBALS['SESSION'], $SESSION);
$GLOBALS['USER'] = new \stdClass();
$GLOBALS['USER']->test = true;
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
$USER = new \stdClass();
$USER->test2 = true;
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
$_SESSION['USER'] = new stdClass();
$_SESSION['USER']->test3 = true;
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
}
public function test_set_user() {
global $USER;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser(0);
$this->assertEquals(0, $USER->id);
$user = $this->getDataGenerator()->create_user();
$this->assertObjectHasAttribute('description', $user);
$this->assertObjectHasAttribute('password', $user);
\core\session\manager::set_user($user);
$this->assertEquals($user->id, $USER->id);
$this->assertObjectNotHasAttribute('description', $user);
$this->assertObjectNotHasAttribute('password', $user);
$this->assertObjectHasAttribute('sesskey', $user);
$this->assertSame($user, $GLOBALS['USER']);
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
}
public function test_login_user() {
global $USER;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser(0);
$this->assertEquals(0, $USER->id);
$user = $this->getDataGenerator()->create_user();
@\core\session\manager::login_user($user); // Ignore header error messages.
$this->assertEquals($user->id, $USER->id);
$this->assertObjectNotHasAttribute('description', $user);
$this->assertObjectNotHasAttribute('password', $user);
$this->assertSame($user, $GLOBALS['USER']);
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
}
public function test_terminate_current() {
global $USER;
global $USER, $SESSION;
$this->resetAfterTest();
// This can not be tested much without real session...
$this->setAdminUser();
\core\session\manager::terminate_current();
$this->assertEquals(0, $USER->id);
$this->assertInstanceOf('stdClass', $SESSION);
$this->assertEmpty((array)$SESSION);
$this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
$this->assertSame($GLOBALS['SESSION'], $SESSION);
$this->assertInstanceOf('stdClass', $USER);
$this->assertEquals(array('id' => 0, 'mnethostid' => 1), (array)$USER, '', 0, 10, true);
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
}
public function test_write_close() {
......@@ -84,6 +168,9 @@ class core_session_manager_testcase extends advanced_testcase {
$userid = $USER->id;
\core\session\manager::write_close();
$this->assertSame($userid, $USER->id);
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
}
public function test_session_exists() {
......@@ -293,22 +380,37 @@ class core_session_manager_testcase extends advanced_testcase {
* @copyright 2103 Rajesh Taneja <rajesh@moodle.com>
*/
public function test_loginas() {
global $USER;
global $USER, $SESSION;
$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.
$adminsession = $SESSION;
$user = $this->getDataGenerator()->create_user();
$_SESSION['extra'] = true;
// Try admin loginas this user in system context.
$this->assertObjectNotHasAttribute('realuser', $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);
$this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
$this->assertSame($GLOBALS['USER'], $USER);
$this->assertNotSame($adminuser, $_SESSION['REALUSER']);
$this->assertEquals($adminuser, $_SESSION['REALUSER']);
$this->assertSame($GLOBALS['SESSION'], $_SESSION['SESSION']);
$this->assertSame($GLOBALS['SESSION'], $SESSION);
$this->assertNotSame($adminsession, $_SESSION['REALSESSION']);
$this->assertEquals($adminsession, $_SESSION['REALSESSION']);
$this->assertArrayNotHasKey('extra', $_SESSION);
// Set user as current user and login as admin user in course context.
\core\session\manager::init_empty_session();
$this->setUser($user);
$this->assertNotEquals($adminuser->id, $USER->id);
$course = $this->getDataGenerator()->create_course();
......@@ -358,6 +460,9 @@ class core_session_manager_testcase extends advanced_testcase {
$user2 = $this->getDataGenerator()->create_user();
$this->setUser($user1);
$normal = \core\session\manager::get_realuser();
$this->assertSame($GLOBALS['USER'], $normal);
\core\session\manager::loginas($user2->id, context_system::instance());
$real = \core\session\manager::get_realuser();
......@@ -370,5 +475,6 @@ class core_session_manager_testcase extends advanced_testcase {
unset($user1->sesskey);
$this->assertEquals($real, $user1);
$this->assertSame($_SESSION['REALUSER'], $real);
}
}
<?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 sessionlib.php file.
*
* @package core
* @category phpunit
* @author Petr Skoda <petr.skoda@totaralms.com>
* @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Unit tests for sessionlib.php file.