Commit 508a65c3 authored by Jeremy's avatar Jeremy

Merge in effort to port memcache-sessions to D7. Still needs work.

Patch thanks to chx.
parent d5a061c8
......@@ -31,12 +31,14 @@ $_memcache_statistics = array('get' => array(), 'set' => array(), 'hit' => array
* @return bool
*/
function dmemcache_set($key, $value, $exp = 0, $bin = 'cache', $mc = NULL) {
global $_memcache_statistics;
$_memcache_statistics['set'][] = $key;
$_memcache_statistics['bins'][] = $bin;
if ($mc || ($mc = dmemcache_object($bin))) {
$full_key = dmemcache_key($key, $bin);
if (!$mc->set($full_key, $value, MEMCACHE_COMPRESSED, $exp)) {
error_log("setting: ". $full_key . gmdate('c') . "\n", 3, '/tmp/log');
if (!memcache_set($mc, $full_key, $value, MEMCACHE_COMPRESSED, $exp)) {
return FALSE;
}
else {
......@@ -60,18 +62,18 @@ function dmemcache_get($key, $bin = 'cache', $mc = NULL) {
$_memcache_statistics['bins'][] = $bin;
if ($mc || ($mc = dmemcache_object($bin))) {
$full_key = dmemcache_key($key, $bin);
$result = $mc->get($full_key);
$result = memcache_get($mc, $full_key);
if ($result) {
// We check $result->expire to see if the object has expired. If so, we
// try and grab a lock. If we get the lock, we return FALSE instead of
// the cached object which should cause it to be rebuilt. If we do not
// get the lock, we return the cached object. The goal here is to avoid
// cache stampedes.
// cache stampedes.
// By default the cache stampede semaphore is held for 15 seconds. This
// can be adjusted by setting the memcache_stampede_semaphore variable.
// TODO: Can we log when a sempahore expires versus being intentionally
// freed to track when this is happening?
if (isset($result->expire) && $result->expire !== CACHE_PERMANENT && $result->expire <= time() && $mc->add($full_key .'_semaphore', '', FALSE, variable_get('memcache_stampede_semaphore', 15))) {
if (isset($result->expire) && $result->expire !== CACHE_PERMANENT && $result->expire <= time() && memcache_add($mc, $full_key .'_semaphore', '', FALSE, variable_get('memcache_stampede_semaphore', 15))) {
$result = FALSE;
}
else {
......@@ -93,7 +95,7 @@ function dmemcache_get($key, $bin = 'cache', $mc = NULL) {
function dmemcache_delete($key, $bin = 'cache', $mc = NULL) {
if ($mc || ($mc = dmemcache_object($bin))) {
$full_key = dmemcache_key($key, $bin);
return $mc->delete($full_key);
return memcache_delete($mc, $full_key);
}
return FALSE;
}
......@@ -110,7 +112,7 @@ function dmemcache_delete($key, $bin = 'cache', $mc = NULL) {
*/
function dmemcache_flush($bin = 'cache', $mc = NULL) {
if ($mc || ($mc = dmemcache_object($bin))) {
return $mc->flush();
return memcache_flush($mc);
}
}
......@@ -126,13 +128,13 @@ function dmemcache_stats($bin = 'cache', $type = '') {
if ($mc = dmemcache_object($bin)) {
// The PHP Memcache extension 3.x version throws an error if the stats
// type is NULL or not in {reset, malloc, slabs, cachedump, items, sizes}.
// If $type is 'default', then no parameter should be passed to the
// If $type is 'default', then no parameter should be passed to the
// Memcache memcache_get_extended_stats() function.
if ($type == 'default' || $type == '') {
return $mc->getExtendedStats();
return memcache_getExtendedStats($mc);
}
else {
return $mc->getExtendedStats($type);
return memcache_getExtendedStats($mc, $type);
}
}
}
......@@ -156,7 +158,7 @@ function dmemcache_object($bin = NULL, $flush = FALSE) {
if ($flush) {
foreach ($memcacheCache as $cluster) {
$cluster->close();
memcache_close($cluster);
}
$memcacheCache = array();
}
......@@ -195,7 +197,7 @@ function dmemcache_object($bin = NULL, $flush = FALSE) {
// This is a server that belongs to this cluster.
if (!$init) {
// First server gets the connect.
if (@$memcache->connect($host, $port)) {
if ($memcache = @memcache_connect($host, $port)) {
// Only set init to true if a connection was made.
$init = TRUE;
$memcache_online = TRUE;
......@@ -206,7 +208,7 @@ function dmemcache_object($bin = NULL, $flush = FALSE) {
}
else {
// Subsequent servers gett addServer.
@$memcache->addServer($host, $port);
memcache_addServer($memcache, $host, $port);
}
}
}
......
......@@ -10,15 +10,15 @@ require_once dirname(__FILE__) . '/dmemcache.inc';
* An alternative to includes/session.inc.
*/
function sess_open($save_path, $session_name) {
function _drupal_session_open() {
return TRUE;
}
function sess_close() {
function _drupal_session_close() {
return TRUE;
}
function sess_read($key) {
function _drupal_session_read($key) {
global $user;
// Write and Close handlers are called after destructing objects since PHP 5.0.5
......@@ -26,20 +26,20 @@ function sess_read($key) {
// So we are moving session closure before destructing objects.
register_shutdown_function('session_write_close');
// Handle the case of first time visitors and clients that don't store
// Handle the case of first time visitors and clients that don't store
// cookies (eg. web crawlers).
if (!isset($_COOKIE[session_name()])) {
$user = drupal_anonymous_user();
return '';
}
// Otherwise, if the session is still active, we have a record of the
// Otherwise, if the session is still active, we have a record of the
// client's session in memcache.
$session = dmemcache_get($key, 'session');
$user = sess_user_load($session);
// Record whether this session contains data so that in sess_write() it can
$user = _memcache_session_user_load($session);
// Record whether this session contains data so that in sess_write() it can
// be determined whether to skip a write.
$user->session_data_present_at_load = !empty($session->session);
......@@ -54,14 +54,14 @@ function sess_read($key) {
* 1a. Without session data.
* 1b. With session data.
* 1c. Session saving has been turned off programatically
* (see session_save_session()).
* (see drupal_save_session()).
* 1d. Without session data but had session data at the beginning of the request
* (thus a write must be made to clear stored session data).
* 2. Authenticated user.
* 2a. Without session data.
* 2b. With session data.
* 2c. Session saving has been turned off programatically
* (see session_save_session()).
* (see drupal_save_session()).
*
* @param $key
* The session ID.
......@@ -70,13 +70,10 @@ function sess_read($key) {
* @return
* TRUE.
*/
function sess_write($key, $value) {
function _drupal_session_write($key, $value) {
global $user;
// If the client doesn't have a session, and one isn't being created ($value),
// do nothing. If session saving has been turned off, do nothing.
// Cases 1a, 1c, and 2c are covered here.
if ((empty($_COOKIE[session_name()]) && empty($value)) || !session_save_session()) {
if (!drupal_save_session()) {
return TRUE;
}
......@@ -90,15 +87,17 @@ function sess_write($key, $value) {
$session->timestamp = time();
// If this is an authenticated user, or there is something to save in the
// session, or this is an anonymous user who currently has nothing in the
// session, or this is an anonymous user who currently has nothing in the
// session but did have something in session storage, write it to memcache.
// If $user->session_data_present_at_load is not set, the current user
// was created during this request and it's safest to do a write.
// was created during this request and it's safest to do a write.
// Cases 1b, 1d, 2a, and 2b are covered here.
if ($user->uid || !empty($value) || empty($value) && (!isset($user->session_data_present_at_load) || $user->session_data_present_at_load)) {
dmemcache_set($key, $session, ini_get('session.gc_maxlifetime'), 'session');
if ($user->uid && $session->timestamp - $user->access > variable_get('session_write_interval', 360)) {
db_query('UPDATE {users} SET access = %d WHERE uid = %d', $session->timestamp, $user->uid);
db_update('users')
->fields(array('access' => $user->uid))
->condition(array('uid' => $user->uid));
// Update the user access time so that the dmemcache_set() call
// caches the updated time.
$user->access = $session->timestamp;
......@@ -109,65 +108,175 @@ function sess_write($key, $value) {
unset($user->session_data_present_at_load);
// Store the session id so we can locate the session with the user id.
$user->sid = $key;
dmemcache_set($user->uid, $user, ini_get('session.gc_maxlifetime'), 'users');
}
return TRUE;
}
function sess_regenerate() {
// We code around http://bugs.php.net/bug.php?id=32802 by destroying
// the session cookie by setting expiration in the past (a negative
// value). This issue only arises in PHP versions before 4.4.0,
// regardless of the Drupal configuration.
// TODO: remove this when we require at least PHP 4.4.0
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time() - 42000, '/');
}
// Store the current (anonymous) session id.
$old_session_id = session_id();
/**
* Called by PHP session handling with the PHP session ID to end a user's session.
*
* @param string $sid
* the session id
*/
function _drupal_session_destroy($sid) {
dmemcache_delete($sid, 'session');
// Reset $_SESSION and $user to prevent a new session from being started
// in drupal_session_commit().
$_SESSION = array();
$user = drupal_anonymous_user();
// Generate the new (authenticated) session id.
session_regenerate_id();
$key = session_id();
// Unset the session cookies.
_drupal_session_delete_cookie(session_name());
}
// Grab the user's information that is cached with the anonymous key.
$info = dmemcache_get($old_session_id, 'session');
function _drupal_session_garbage_collection($lifetime) {
// Automatic with memcached.
// Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
// value. For example, if you want user sessions to stay in your database
// for three weeks before deleting them, you need to set gc_maxlifetime
// to '1814400'. At that value, only after a user doesn't log in after
// three weeks (1814400 seconds) will his/her session be removed.
return TRUE;
}
// Update it.
$info->sid = $key;
/**
* Initialize the session handler, starting a session if needed.
*/
function drupal_session_initialize() {
global $user;
// Store it with the new key.
dmemcache_set($key, $info, ini_get('session.gc_maxlifetime'), 'session');
session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
// Clear the old data from the cache.
dmemcache_delete($old_session_id, 'session');
if (isset($_COOKIE[session_name()])) {
// If a session cookie exists, initialize the session. Otherwise the
// session is only started on demand in drupal_session_commit(), making
// anonymous users not use a session cookie unless something is stored in
// $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
drupal_session_start();
if (!empty($user->uid) || !empty($_SESSION)) {
drupal_page_is_cacheable(FALSE);
}
}
else {
// Set a session identifier for this request. This is necessary because
// we lazyly start sessions at the end of this request, and some
// processes (like drupal_get_token()) needs to know the future
// session ID in advance.
$user = drupal_anonymous_user();
session_id(md5(uniqid('', TRUE)));
}
}
/**
* Counts how many users have sessions. Can count either anonymous sessions, authenticated sessions, or both.
* Would be insane slow with memcached as we would need to retrieve at least the stats of all object.
* Not implemented.
*/
function sess_count($timestamp = 0, $anonymous = true) {
function drupal_session_count($timestamp = 0, $anonymous = true) {
}
/**
* Called by PHP session handling with the PHP session ID to end a user's session.
* Forcefully start a session, preserving already set session data.
*
* @param string $sid
* the session id
* @ingroup php_wrappers
*/
function sess_destroy_sid($sid) {
dmemcache_delete($sid, 'session');
function drupal_session_start() {
if (!drupal_session_started()) {
// Save current session data before starting it, as PHP will destroy it.
$session_data = isset($_SESSION) ? $_SESSION : NULL;
session_start();
drupal_session_started(TRUE);
// Restore session data.
if (!empty($session_data)) {
$_SESSION += $session_data;
}
}
}
/**
* Commit the current session, if necessary.
*
* If an anonymous user already have an empty session, destroy it.
*/
function drupal_session_commit() {
global $user;
if (!drupal_save_session()) {
// We don't have anything to do if we are not allowed to save the session.
return;
}
if (empty($user->uid) && empty($_SESSION)) {
// There is no session data to store, destroy the session if it was
// previously started.
if (drupal_session_started()) {
session_destroy();
}
}
else {
// There is session data to store. Start the session if it is not already
// started.
if (!drupal_session_started()) {
drupal_session_start();
}
// Write the session data.
session_write_close();
}
}
/**
* Return whether a session has been started.
*/
function drupal_session_started($set = NULL) {
static $session_started = FALSE;
if (isset($set)) {
$session_started = $set;
}
return $session_started && session_id();
}
/**
* Called when an anonymous user becomes authenticated or vice-versa.
*
* @ingroup php_wrappers
*/
function drupal_session_regenerate() {
global $user;
if (drupal_session_started()) {
$old_session_id = session_id();
session_regenerate_id();
$new_session_id = session_id();
}
else {
// Start the session when it doesn't exist yet.
// Preserve the logged in user, as it will be reset to anonymous
// by _drupal_session_read.
$account = $user;
drupal_session_start();
$user = $account;
}
if (isset($old_session_id)) {
$info = dmemcache_get($old_session_id, 'session');
$info->sid = $new_session_id;
dmemcache_set($new_session_id, $info, ini_get('session.gc_maxlifetime'), 'session');
// Clear the old data from the cache.
dmemcache_delete($old_session_id, 'session');
}
}
/**
* End a specific user's session.
*/
function sess_destroy_uid($uid) {
function drupal_session_destroy_uid($uid) {
$user = dmemcache_get($uid, 'users');
if (is_object($user) && isset($user->sid)) {
dmemcache_delete($user->sid, 'session');
......@@ -175,15 +284,6 @@ function sess_destroy_uid($uid) {
dmemcache_delete($uid, 'users');
}
function sess_gc($lifetime) {
// Automatic with memcached.
// Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
// value. For example, if you want user sessions to stay in your database
// for three weeks before deleting them, you need to set gc_maxlifetime
// to '1814400'. At that value, only after a user doesn't log in after
// three weeks (1814400 seconds) will his/her session be removed.
return TRUE;
}
/**
* Determine whether to save session data of the current request.
......@@ -197,8 +297,8 @@ function sess_gc($lifetime) {
* @return
* FALSE if writing session data has been disabled. Otherwise, TRUE.
*/
function session_save_session($status = NULL) {
static $save_session = TRUE;
function drupal_save_session($status = NULL) {
$save_session = &drupal_static(__FUNCTION__, TRUE);
if (isset($status)) {
$save_session = $status;
}
......@@ -213,7 +313,7 @@ function session_save_session($status = NULL) {
* @return $user
* The user object.
*/
function sess_user_load($session) {
function _memcache_session_user_load($session) {
// We found the client's session record and they are an authenticated user.
if ($session && $session->uid != 0) {
$user = dmemcache_get($session->uid, 'users');
......@@ -221,8 +321,8 @@ function sess_user_load($session) {
// If the cached user was not found in the 'users' memcache bin, $user will
// be FALSE.
// In either of these cases, the user must be retrieved from the database.
if (!$user->uid && isset($session->uid) && $session->uid != 0) {
$user = db_fetch_object(db_query('SELECT u.* FROM {users} u WHERE u.uid = %d', $session->uid));
if (!$user && isset($session->uid) && $session->uid != 0) {
$user = db_query('SELECT u.* FROM {users} u WHERE u.uid = :uid', array('uid' => $session->uid))->fetchObject();
$user = drupal_unpack($user);
// Normally we would join the session and user tables. But we already
......@@ -236,8 +336,8 @@ function sess_user_load($session) {
// Add roles element to $user
$user->roles = array();
$user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
$result = db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = %d", $user->uid);
while ($role = db_fetch_object($result)) {
$result = db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = :uid", array('uid' => $user->uid));
foreach ($result as $role) {
$user->roles[$role->rid] = $role->name;
}
}
......@@ -266,3 +366,20 @@ function sess_user_load($session) {
return $user;
}
/**
* Deletes the session cookie.
*
* @param $name
* Name of session cookie to delete.
* @param $force_insecure
* Fornce cookie to be insecure.
*/
function _drupal_session_delete_cookie($name, $force_insecure = FALSE) {
if (isset($_COOKIE[$name])) {
$params = session_get_cookie_params();
setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], !$force_insecure && $params['secure'], $params['httponly']);
unset($_COOKIE[$name]);
}
}
......@@ -222,7 +222,7 @@ class MemCacheDrupal implements DrupalCacheInterface {
}
}
}
function isEmpty() {
// We do not know so err on the safe side?
return FALSE;
......
......@@ -2,3 +2,7 @@
name = Memcache
description = High performance integration with memcache.
package = Caching
core = 7.x
files[] = memcache.inc
files[] = memcache.module
files[] = memcache.test
......@@ -318,3 +318,225 @@ class MemCacheClearCase extends MemcacheTestCase {
t('All cache entries removed when the array exceeded the cache clear threshold.'));
}
}
/**
* @file
* Provides SimpleTests for core session handling functionality.
*/
class MemcacheSessionTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Session tests',
'description' => 'Memcache session handling tests.',
'group' => 'Memcache'
);
}
function setUp() {
parent::setUp('session_test');
}
/**
* Tests for drupal_save_session() and drupal_session_regenerate().
*/
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'));
// Test session hardening code from SA-2008-044.
$user = $this->drupalCreateUser(array('access content'));
// Enable sessions.
$this->sessionReset($user->uid);
// 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.'));
$this->drupalLogout();
// 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.'));
$original_session = $matches[1];
// 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'));
$this->drupalGet('user');
$pass = $this->assertText($user->name, t('Found name: %name', array('%name' => $user->name)), t('User login'));
$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.'));
}
/**
* Test data persistence via the session_test module callbacks. Also tests
* drupal_session_count() since session data is already generated here.
*/
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'));
$this->drupalGet('session-test/get');
$this->assertText($value_1, t('Session correctly returned the stored data for an authenticated user.'), t('Session'));
// Attempt to write over val_1. If drupal_save_session(FALSE) is working.
// 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'));
$this->drupalGet('session-test/get');
$this->assertText($value_1, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session'));
// 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'));
// 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'));
// Now try to store some data as an anonymous user.
$value_3 = $this->randomName();
$this->drupalGet('session-test/set/' . $value_3);
$this->assertText($value_3, t('Session data stored for anonymous user.'), t('Session'));
$this->drupalGet('session-test/get');
$this->assertText($value_3, t('Session correctly returned the stored data for an anonymous user.'), t('Session'));
// Try to store data when drupal_save_session(FALSE).
$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'));
$this->drupalGet('session-test/get');
$this->assertText($value_3, t('Session data is not saved for drupal_save_session(FALSE).'), t('Session'));
// Login, the data should persist.
$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'));
// Change session and create another user.
$user2 = $this->drupalCreateUser(array('access content'));
$this->sessionReset($user2->uid);
$this->drupalLogin($user2);
}
/**