Newer
Older

Dries Buytaert
committed
<?php
// $Id$
/**
* @file
* Provides SimpleTests for core session handling functionality.
*/
class SessionTestCase extends DrupalWebTestCase {

Angie Byron
committed
public static function getInfo() {

Dries Buytaert
committed
return array(
'name' => 'Session tests',
'description' => 'Drupal session handling tests.',
'group' => 'Session'

Dries Buytaert
committed
);
}
function setUp() {
parent::setUp('session_test');
}

Dries Buytaert
committed
/**

Dries Buytaert
committed
* Tests for drupal_save_session() and drupal_session_regenerate().

Dries Buytaert
committed
*/

Dries Buytaert
committed
function testSessionSaveRegenerate() {
$this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE (inside of testing framework) when initially called with no arguments.'), t('Session'));
$this->assertFalse(drupal_save_session(FALSE), t('drupal_save_session() correctly returns FALSE when called with FALSE.'), t('Session'));
$this->assertFalse(drupal_save_session(), t('drupal_save_session() correctly returns FALSE when saving has been disabled.'), t('Session'));
$this->assertTrue(drupal_save_session(TRUE), t('drupal_save_session() correctly returns TRUE when called with TRUE.'), t('Session'));
$this->assertTrue(drupal_save_session(), t('drupal_save_session() correctly returns TRUE when saving has been enabled.'), t('Session'));

Dries Buytaert
committed
// Test session hardening code from SA-2008-044.
$user = $this->drupalCreateUser(array('access content'));

Dries Buytaert
committed

Dries Buytaert
committed
// Enable sessions.
$this->sessionReset($user->uid);

Dries Buytaert
committed

Dries Buytaert
committed
// Make sure the session cookie is set as HttpOnly.
$this->drupalLogin($user);
$this->assertTrue(preg_match('/HttpOnly/i', $this->drupalGetHeader('Set-Cookie', TRUE)), t('Session cookie is set as HttpOnly.'));

Dries Buytaert
committed
$this->drupalLogout();

Dries Buytaert
committed

Dries Buytaert
committed
// Verify that the session is regenerated if a module calls exit
// in hook_user_login().
user_save($user, array('name' => 'session_test_user'));
$user->name = 'session_test_user';
$this->drupalGet('session-test/id');
$matches = array();
preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches);
$this->assertTrue(!empty($matches[1]) , t('Found session ID before logging in.'));

Dries Buytaert
committed
$original_session = $matches[1];

Dries Buytaert
committed

Dries Buytaert
committed
// We cannot use $this->drupalLogin($user); because we exit in
// session_test_user_login() which breaks a normal assertion.
$edit = array(
'name' => $user->name,
'pass' => $user->pass_raw
);
$this->drupalPost('user', $edit, t('Log in'));

Dries Buytaert
committed
$this->drupalGet('user');
$pass = $this->assertText($user->name, t('Found name: %name', array('%name' => $user->name)), t('User login'));

Dries Buytaert
committed
$this->_logged_in = $pass;
$this->drupalGet('session-test/id');
$matches = array();
preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches);
$this->assertTrue(!empty($matches[1]) , t('Found session ID after logging in.'));
$this->assertTrue($matches[1] != $original_session, t('Session ID changed after login.'));

Dries Buytaert
committed
}
/**
* Test data persistence via the session_test module callbacks. Also tests

Dries Buytaert
committed
* drupal_session_count() since session data is already generated here.

Dries Buytaert
committed
*/
function testDataPersistence() {
$user = $this->drupalCreateUser(array('access content'));
// Enable sessions.
$this->sessionReset($user->uid);
$this->drupalLogin($user);
$value_1 = $this->randomName();
$this->drupalGet('session-test/set/' . $value_1);
$this->assertText($value_1, t('The session value was stored.'), t('Session'));

Dries Buytaert
committed
$this->drupalGet('session-test/get');
$this->assertText($value_1, t('Session correctly returned the stored data for an authenticated user.'), t('Session'));

Dries Buytaert
committed

Dries Buytaert
committed
// Attempt to write over val_1. If drupal_save_session(FALSE) is working.

Dries Buytaert
committed
// properly, val_1 will still be set.
$value_2 = $this->randomName();
$this->drupalGet('session-test/no-set/' . $value_2);
$this->assertText($value_2, t('The session value was correctly passed to session-test/no-set.'), t('Session'));

Dries Buytaert
committed
$this->drupalGet('session-test/get');
$this->assertText($value_1, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session'));

Dries Buytaert
committed
// Switch browser cookie to anonymous user, then back to user 1.
$this->sessionReset();
$this->sessionReset($user->uid);
$this->assertText($value_1, t('Session data persists through browser close.'), t('Session'));

Dries Buytaert
committed
// Logout the user and make sure the stored value no longer persists.
$this->drupalLogout();
$this->sessionReset();
$this->drupalGet('session-test/get');
$this->assertNoText($value_1, t("After logout, previous user's session data is not available."), t('Session'));

Dries Buytaert
committed

Dries Buytaert
committed
// Now try to store some data as an anonymous user.

Dries Buytaert
committed
$value_3 = $this->randomName();
$this->drupalGet('session-test/set/' . $value_3);
$this->assertText($value_3, t('Session data stored for anonymous user.'), t('Session'));

Dries Buytaert
committed
$this->drupalGet('session-test/get');
$this->assertText($value_3, t('Session correctly returned the stored data for an anonymous user.'), t('Session'));

Dries Buytaert
committed

Dries Buytaert
committed
// Try to store data when drupal_save_session(FALSE).

Dries Buytaert
committed
$value_4 = $this->randomName();
$this->drupalGet('session-test/no-set/' . $value_4);
$this->assertText($value_4, t('The session value was correctly passed to session-test/no-set.'), t('Session'));

Dries Buytaert
committed
$this->drupalGet('session-test/get');
$this->assertText($value_3, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session'));

Dries Buytaert
committed

Dries Buytaert
committed
// Login, the data should persist.

Dries Buytaert
committed
$this->drupalLogin($user);
$this->sessionReset($user->uid);
$this->drupalGet('session-test/get');
$this->assertNoText($value_1, t('Session has persisted for an authenticated user after logging out and then back in.'), t('Session'));

Dries Buytaert
committed

Dries Buytaert
committed
// Change session and create another user.

Dries Buytaert
committed
$user2 = $this->drupalCreateUser(array('access content'));
$this->sessionReset($user2->uid);
$this->drupalLogin($user2);
}

Dries Buytaert
committed
/**
* Test that empty anonymous sessions are destroyed.
*/
function testEmptyAnonymousSession() {

Dries Buytaert
committed
// Verify that no session is automatically created for anonymous user.

Dries Buytaert
committed
$this->drupalGet('');
$this->assertSessionCookie(FALSE);
$this->assertSessionEmpty(TRUE);

Dries Buytaert
committed
// The same behavior is expected when caching is enabled.

Dries Buytaert
committed
variable_set('cache', 1);

Dries Buytaert
committed
$this->drupalGet('');

Dries Buytaert
committed
$this->assertSessionCookie(FALSE);

Dries Buytaert
committed
$this->assertSessionEmpty(TRUE);
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Page was not cached.'));

Dries Buytaert
committed
// Start a new session by setting a message.
$this->drupalGet('session-test/set-message');

Dries Buytaert
committed
$this->assertSessionCookie(TRUE);
$this->assertTrue($this->drupalGetHeader('Set-Cookie'), t('New session was started.'));

Dries Buytaert
committed

Dries Buytaert
committed
// Display the message, during the same request the session is destroyed
// and the session cookie is unset.

Dries Buytaert
committed
$this->drupalGet('');

Dries Buytaert
committed
$this->assertSessionCookie(FALSE);

Dries Buytaert
committed
$this->assertSessionEmpty(FALSE);
$this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), t('Caching was bypassed.'));
$this->assertText(t('This is a dummy message.'), t('Message was displayed.'));
$this->assertTrue(preg_match('/SESS\w+=deleted/', $this->drupalGetHeader('Set-Cookie')), t('Session cookie was deleted.'));

Dries Buytaert
committed
// Verify that session was destroyed.
$this->drupalGet('');
$this->assertSessionCookie(FALSE);

Dries Buytaert
committed
$this->assertSessionEmpty(TRUE);
$this->assertNoText(t('This is a dummy message.'), t('Message was not cached.'));
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Page was cached.'));
$this->assertFalse($this->drupalGetHeader('Set-Cookie'), t('New session was not started.'));

Dries Buytaert
committed
// Verify that no session is created if drupal_save_session(FALSE) is called.
$this->drupalGet('session-test/set-message-but-dont-save');
$this->assertSessionCookie(FALSE);
$this->assertSessionEmpty(TRUE);
// Verify that no message is displayed.
$this->drupalGet('');
$this->assertSessionCookie(FALSE);
$this->assertSessionEmpty(TRUE);
$this->assertNoText(t('This is a dummy message.'), t('The message was not saved.'));

Dries Buytaert
committed
}
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/**
* Test that sessions are only saved when necessary.
*/
function testSessionWrite() {
$user = $this->drupalCreateUser(array('access content'));
$this->drupalLogin($user);
$sql = 'SELECT u.access, s.timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid';
$times1 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
// Before every request we sleep one second to make sure that if the session
// is saved, its timestamp will change.
// Modify the session.
sleep(1);
$this->drupalGet('session-test/set/foo');
$times2 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
$this->assertEqual($times2->access, $times1->access, t('Users table was not updated.'));
$this->assertNotEqual($times2->timestamp, $times1->timestamp, t('Sessions table was updated.'));
// Write the same value again, i.e. do not modify the session.
sleep(1);
$this->drupalGet('session-test/set/foo');
$times3 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
$this->assertEqual($times3->access, $times1->access, t('Users table was not updated.'));
$this->assertEqual($times3->timestamp, $times2->timestamp, t('Sessions table was not updated.'));
// Do not change the session.
sleep(1);
$this->drupalGet('');
$times4 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
$this->assertEqual($times4->access, $times3->access, t('Users table was not updated.'));
$this->assertEqual($times4->timestamp, $times3->timestamp, t('Sessions table was not updated.'));
// Force updating of users and sessions table once per second.
variable_set('session_write_interval', 0);
$this->drupalGet('');
$times5 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
$this->assertNotEqual($times5->access, $times4->access, t('Users table was updated.'));
$this->assertNotEqual($times5->timestamp, $times4->timestamp, t('Sessions table was updated.'));
}

Dries Buytaert
committed
/**
* Reset the cookie file so that it refers to the specified user.
*
* @param $uid User id to set as the active session.
*/
function sessionReset($uid = 0) {
// Close the internal browser.
$this->curlClose();

Dries Buytaert
committed
$this->loggedInUser = FALSE;

Dries Buytaert
committed
// Change cookie file for user.
$this->cookieFile = file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath() . '/cookie.' . $uid . '.txt';
$this->additionalCurlOptions[CURLOPT_COOKIEFILE] = $this->cookieFile;
$this->additionalCurlOptions[CURLOPT_COOKIESESSION] = TRUE;

Dries Buytaert
committed
$this->drupalGet('session-test/get');
$this->assertResponse(200, t('Session test module is correctly enabled.'), t('Session'));

Dries Buytaert
committed
}

Dries Buytaert
committed
/**
* Assert whether the SimpleTest browser sent a session cookie.
*/
function assertSessionCookie($sent) {
if ($sent) {
$this->assertNotNull($this->session_id, t('Session cookie was sent.'));

Dries Buytaert
committed
}
else {
$this->assertNull($this->session_id, t('Session cookie was not sent.'));

Dries Buytaert
committed
}
}
/**
* Assert whether $_SESSION is empty at the beginning of the request.
*/
function assertSessionEmpty($empty) {
if ($empty) {
$this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '1', t('Session was empty.'));

Dries Buytaert
committed
}
else {
$this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', t('Session was not empty.'));

Dries Buytaert
committed
}
}

Dries Buytaert
committed
}

Dries Buytaert
committed
/**
* Ensure that when running under https two session cookies are generated.
*/
class SessionHttpsTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Session https handling',
'description' => 'Ensure that when running under https two session cookies are generated.',
'group' => 'Session'
);
}
public function setUp() {
parent::setUp('session_test');
}
protected function testHttpsSession() {
global $is_https;

Angie Byron
committed
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
if ($is_https) {
$secure_session_name = session_name();
$insecure_session_name = substr(session_name(), 1);
}
else {
$secure_session_name = 'S' . session_name();
$insecure_session_name = session_name();
}
$user = $this->drupalCreateUser(array('access administration pages'));
// Test HTTPS session handling by altering the form action to submit the
// login form through https.php, which creates a mock HTTPS request.
$this->drupalGet('user');
$form = $this->xpath('//form[@id="user-login"]');
$form[0]['action'] = $this->httpsUrl('user');
$edit = array('name' => $user->name, 'pass' => $user->pass_raw);
$this->drupalPost(NULL, $edit, t('Log in'));
// Test a second concurrent session.
$this->curlClose();
$this->drupalGet('user');
$form = $this->xpath('//form[@id="user-login"]');
$form[0]['action'] = $this->httpsUrl('user');
$this->drupalPost(NULL, $edit, t('Log in'));
// Check secure cookie on secure page.
$this->assertTrue($this->cookies[$secure_session_name]['secure'], 'The secure cookie has the secure attribute');
// Check insecure cookie is not set.
$this->assertFalse(isset($this->cookies[$insecure_session_name]));

Angie Byron
committed
$ssid = $this->cookies[$secure_session_name]['value'];

Dries Buytaert
committed
$this->assertSessionIds('', $ssid, 'Session has NULL for SID and a correct secure SID.');

Angie Byron
committed
$cookie = $secure_session_name . '=' . $ssid;

Angie Byron
committed
// Verify that user is logged in on secure URL.
$this->curlClose();

Dries Buytaert
committed
$this->drupalGet($this->httpsUrl('admin/config'), array(), array('Cookie: ' . $cookie));
$this->assertText(t('Configuration'));

Angie Byron
committed
// Verify that user is not logged in on non-secure URL.
if (!$is_https) {
$this->curlClose();

Dries Buytaert
committed
$this->drupalGet('admin/config', array(), array('Cookie: ' . $cookie));
$this->assertNoText(t('Configuration'));
$this->assertResponse(403);

Angie Byron
committed
}
// Clear browser cookie jar.
$this->cookies = array();

Dries Buytaert
committed
if ($is_https) {
// The functionality does not make sense when running on https.
return;
}
// Enable secure pages.
variable_set('https', TRUE);
$this->curlClose();

Dries Buytaert
committed
// Start an anonymous session on the insecure site.
$session_data = $this->randomName();
$this->drupalGet('session-test/set/' . $session_data);

Dries Buytaert
committed
// Check secure cookie on insecure page.
$this->assertFalse(isset($this->cookies[$secure_session_name]), 'The secure cookie is not sent on insecure pages.');
// Check insecure cookie on insecure page.
$this->assertFalse($this->cookies[$insecure_session_name]['secure'], 'The insecure cookie does not have the secure attribute');

Dries Buytaert
committed
// Store the anonymous cookie so we can validate that its session is killed
// after login.
$anonymous_cookie = $insecure_session_name . '=' . $this->cookies[$insecure_session_name]['value'];

Dries Buytaert
committed
// Check that password request form action is not secure.
$this->drupalGet('user/password');
$form = $this->xpath('//form[@id="user-pass"]');
$this->assertNotEqual(substr($form[0]['action'], 0, 6), 'https:', 'Password request form action is not secure');
$form[0]['action'] = $this->httpsUrl('user');
// Check that user login form action is secure.
$this->drupalGet('user');
$form = $this->xpath('//form[@id="user-login"]');

Dries Buytaert
committed
$this->assertEqual(substr($form[0]['action'], 0, 6), 'https:', 'Login form action is secure');
$form[0]['action'] = $this->httpsUrl('user');
$edit = array(
'name' => $user->name,
'pass' => $user->pass_raw,
);
$this->drupalPost(NULL, $edit, t('Log in'));
// Check secure cookie on secure page.
$this->assertTrue($this->cookies[$secure_session_name]['secure'], 'The secure cookie has the secure attribute');
// Check insecure cookie on secure page.
$this->assertFalse($this->cookies[$insecure_session_name]['secure'], 'The insecure cookie does not have the secure attribute');

Angie Byron
committed
$sid = $this->cookies[$insecure_session_name]['value'];
$ssid = $this->cookies[$secure_session_name]['value'];
$this->assertSessionIds($sid, $ssid, 'Session has both secure and insecure SIDs');

Dries Buytaert
committed
$cookies = array(

Angie Byron
committed
$insecure_session_name . '=' . $sid,
$secure_session_name . '=' . $ssid,

Dries Buytaert
committed
);

Dries Buytaert
committed
// Test that session data saved before login is still available on the
// authenticated session.
$this->drupalGet('session-test/get');
$this->assertText($session_data, 'Session correctly returned the stored data set by the anonymous session.');

Dries Buytaert
committed
foreach ($cookies as $cookie_key => $cookie) {

Dries Buytaert
committed
foreach (array('admin/config', $this->httpsUrl('admin/config')) as $url_key => $url) {

Dries Buytaert
committed
$this->curlClose();
$this->drupalGet($url, array(), array('Cookie: ' . $cookie));
if ($cookie_key == $url_key) {

Dries Buytaert
committed
$this->assertText(t('Configuration'));

Dries Buytaert
committed
}
else {

Dries Buytaert
committed
$this->assertNoText(t('Configuration'));
$this->assertResponse(403);

Dries Buytaert
committed
}
}
}

Dries Buytaert
committed
// Test that session data saved before login is not available using the

Dries Buytaert
committed
// pre-login anonymous cookie.
$this->cookies = array();
$this->drupalGet('session-test/get', array('Cookie: ' . $anonymous_cookie));
$this->assertNoText($session_data, 'Initial anonymous session is inactive after login.');
// Clear browser cookie jar.
$this->cookies = array();
// Start an anonymous session on the secure site.
$this->drupalGet($this->httpsUrl('session-test/set/1'));
// Mock a login to the secure site using the secure session cookie.
$this->drupalGet('user');
$form = $this->xpath('//form[@id="user-login"]');
$form[0]['action'] = $this->httpsUrl('user');
$this->drupalPost(NULL, $edit, t('Log in'), array(), array('Cookie: ' . $secure_session_name . '=' . $this->cookies[$secure_session_name]['value']));
// Get the insecure session cookie set by the secure login POST request.
$headers = $this->drupalGetHeaders(TRUE);
strtok($headers[0]['set-cookie'], ';=');
$session_id = strtok(';=');
// Test that the user is also authenticated on the insecure site.
$this->drupalGet("user/{$user->uid}/edit", array(), array('Cookie: ' . $insecure_session_name . '=' . $session_id));
$this->assertResponse(200);

Dries Buytaert
committed
}

Angie Byron
committed
/**
* Test that there exists a session with two specific session IDs.
*
* @param $sid
* The insecure session ID to search for.
* @param $ssid
* The secure session ID to search for.
* @param $assertion_text
* The text to display when we perform the assertion.
*
* @return
* The result of assertTrue() that there's a session in the system that
* has the given insecure and secure session IDs.
*/
protected function assertSessionIds($sid, $ssid, $assertion_text) {
$args = array(
':sid' => $sid,
':ssid' => $ssid,
);

Dries Buytaert
committed
return $this->assertTrue(db_query('SELECT timestamp FROM {sessions} WHERE sid = :sid AND ssid = :ssid', $args)->fetchField(), $assertion_text);

Angie Byron
committed
}

Dries Buytaert
committed
protected function httpsUrl($url) {
global $base_url;
return $base_url . '/modules/simpletest/tests/https.php?q=' . $url;
}
}