diff --git mongodb_session/mongodb_session.inc mongodb_session/mongodb_session.inc index 1ff62c5..b0660f7 100644 --- mongodb_session/mongodb_session.inc +++ mongodb_session/mongodb_session.inc @@ -8,12 +8,12 @@ include_once dirname(__FILE__) . '/../mongodb.module'; * User session handling functions. * * The user-level session storage handlers: - * - _drupal_session_open() - * - _drupal_session_close() - * - _drupal_session_read() - * - _drupal_session_write() - * - _drupal_session_destroy() - * - _drupal_session_garbage_collection() + * - sess_open() + * - sess_close() + * - sess_read() + * - sess_write() + * - sess_destroy_sid() + * - sess_gc() * are assigned by session_set_save_handler() in bootstrap.inc and are called * automatically by PHP. These functions should not be called directly. Session * data should instead be accessed via the $_SESSION superglobal. @@ -31,7 +31,7 @@ include_once dirname(__FILE__) . '/../mongodb.module'; * @return * This function will always return TRUE. */ -function _drupal_session_open() { +function sess_open() { return TRUE; } @@ -47,7 +47,7 @@ function _drupal_session_open() { * @return * This function will always return TRUE. */ -function _drupal_session_close() { +function sess_close() { return TRUE; } @@ -67,8 +67,8 @@ function _drupal_session_close() { * Either an array of the session data, or an empty string, if no data * was found or the user is anonymous. */ -function _drupal_session_read($sid) { - global $user, $is_https; +function sess_read($sid) { + global $user; // Write and Close handlers are called after destructing objects // since PHP 5.0.5. @@ -78,46 +78,44 @@ function _drupal_session_read($sid) { // Handle the case of first time visitors and clients that don't store // cookies (eg. web crawlers). - $insecure_session_name = substr(session_name(), 1); - if (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name])) { + if (!isset($_COOKIE[session_name()])) { $user = drupal_anonymous_user(); + sess_debug('Sess Read: No Cookie'); return ''; } // Otherwise, if the session is still active, we have a record of the - // client's session in the database. If it's HTTPS then we are either have - // a HTTPS session or we are about to log in so we check the sessions table - // for an anonymous session wth the non-HTTPS-only cookie. + // client's session in the database. $collection = mongodb_collection(variable_get('mongodb_session', 'session')); - if ($is_https) { - $user = (object)$collection->findOne(array('ssid' => $sid)); - if (!$user) { - if (isset($_COOKIE[$insecure_session_name])) { - $user = (object)$collection->findOne(array('sid' => $_COOKIE[$insecure_session_name], 'uid' => 0)); + $session = (object) $collection->findOne(array('sid' => $sid)); + sess_debug($session, 'Sess Read: ' . $sid); + + // We found the client's session record and they are an authenticated, + // active user. + if ($session && $session->uid > 0) { + $user = db_fetch_object(db_query("SELECT * FROM {users} WHERE uid = %d", $session->uid)); + $user = (object) array_merge((array) $user, (array) $session); + if ($user->status == 1){ + // This is done to unserialize the data member of $user + $user = drupal_unpack($user); + + // 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)) { + $user->roles[$role->rid] = $role->name; } } - } - else { - $r = $collection->find(array('sid' => $sid)); - foreach ($r as $user); - $user = (object)$user; + sess_debug('Uid: ' . $user->uid . ' Name: ' . $user->name, "Sess Read: Found User"); } - // We found the client's session record and they are an authenticated user. - if ($user && !empty($user->uid)) { - // This is done to unserialize the data member of $user. - $user = drupal_unpack($user); - - // Add roles element to $user. - $user->roles = array(); - $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; - $user->roles += 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))->fetchAllKeyed(0, 1); - } - // We didn't find the client's record (session has expired), or they - // are an anonymous user. - else { + // We didn't find the client's record (session has expired), or they are + // blocked, or they are an anonymous user. + if (!$session || $session->uid == 0 || !$user->status) { $session = isset($user->session) ? $user->session : ''; $user = drupal_anonymous_user($session); + sess_debug('Uid: ' . $user->uid . ' Status: ' . $user->status, 'Sess Read: Anon or Blocked'); } return $user->session; @@ -139,58 +137,159 @@ function _drupal_session_read($sid) { * @return * This function will always return TRUE. */ -function _drupal_session_write($sid, $value) { - global $user, $is_https; +function sess_write($sid, $value) { + global $user; - if (!drupal_save_session()) { - // We don't have anything to do if we are not allowed to save the session. - return; + // If saving of session data is disabled, or if a new empty anonymous session + // has been started, do nothing. This keeps anonymous users, including + // crawlers, out of the session table, unless they actually have something + // stored in $_SESSION. + if (!session_save_session() || ($user->uid == 0 && empty($_COOKIE[session_name()]) && empty($value))) { + return TRUE; } + $request_time = time(); $fields = array( 'uid' => $user->uid, 'cache' => isset($user->cache) ? (int)$user->cache : 0, 'hostname' => ip_address(), 'session' => $value, - 'timestamp' => REQUEST_TIME, + 'timestamp' => $request_time, ); - $key = array('sid' => $sid); - if ($is_https) { - $key['ssid'] = $sid; - $insecure_session_name = substr(session_name(), 1); - // The "secure pages" setting allows a site to simultaneously use both - // secure and insecure session cookies. If enabled, use the insecure session - // identifier as the sid. - if (variable_get('https', FALSE) && isset($_COOKIE[$insecure_session_name])) { - $key['sid'] = $_COOKIE[$insecure_session_name]; - } - } - $collection = mongodb_collection(variable_get('mongodb_session', 'session')); - $collection - ->update($key, $key + $fields + (array)$user, array('upsert' => TRUE)); + $update = $collection->update(array('sid' => $sid), array('$set' => $fields), array('upsert' => TRUE)); + sess_debug($update, 'Sess Write: ' . $sid); // Last access time is updated no more frequently than once every 180 seconds. // This reduces contention in the users table. - if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) { - db_update('users') - ->fields(array( - 'access' => REQUEST_TIME - )) - ->condition('uid', $user->uid) - ->execute(); + if ($user->uid && $request_time - $user->access > variable_get('session_write_interval', 180)) { + db_query("UPDATE {users} SET access = %d WHERE uid = %d", $request_time, $user->uid); + } + + return TRUE; +} + +/** + * Called when an anonymous user becomes authenticated or vice-versa. + * + * This backport obsoletes this function, but a call to the new function + * is kept here to maximize API consistency. + */ +function sess_regenerate() { + drupal_session_regenerate(); +} + +/** + * Counts how many users have sessions. Can count either anonymous sessions or authenticated sessions. + * + * @param int $timestamp + * A Unix timestamp representing a point of time in the past. + * The default is 0, which counts all existing sessions. + * @param boolean $anonymous + * TRUE counts only anonymous users. + * FALSE counts only authenticated users. + * @return int + * The number of users with sessions. + */ +function sess_count($timestamp = 0, $anonymous = true) { + sess_debug('Sess Count'); + $collection = mongodb_collection(variable_get('mongodb_session', 'session')); + $conditions = array( + 'timestamp' => array('$gte' => $timestamp), + ); + if ($anonymous) { + $conditions['uid'] = 0; + } + else { + $conditions['uid'] = array('$gt' => 0); + } + return $collection->find($conditions)->count(); +} + +/** + * Called by PHP session handling with the PHP session ID to end a user's session. + * + * @param string $key + * the session id + */ +function sess_destroy_sid($sid) { + // Delete session data. + $collection = mongodb_collection(variable_get('mongodb_session', 'session')); + $collection->remove(array('sid' => $sid)); + sess_debug('Sess Destroy: ' . $sid); + + // If the session ID being destroyed is the one of the current user, + // clean-up his/her session data and cookie. + if ($key == session_id()) { + global $user; + + // Reset $_SESSION and $user to prevent a new session from being started + // in drupal_session_commit() + $_SESSION = array(); + $user = drupal_anonymous_user(); + + // Unset the session cookie. + if (isset($_COOKIE[session_name()])) { + $params = session_get_cookie_params(); + + if (version_compare(PHP_VERSION, '5.2.0') === 1) { + setcookie(session_name(), '', $_SERVER['REQUEST_TIME'] - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + } + else { + setcookie(session_name(), '', $_SERVER['REQUEST_TIME'] - 3600, $params['path'], $params['domain'], $params['secure']); + } + unset($_COOKIE[session_name()]); + } } +} + +/** + * End a specific user's session + * + * @param string $uid + * the user id + */ +function sess_destroy_uid($uid) { + $collection = mongodb_collection(variable_get('mongodb_session', 'session')); + $collection->remove(array('uid' => $uid)); + sess_debug('Sess Detroy UID: ' . $uid); +} +/** + * Session handler assigned by session_set_save_handler(). + * + * Cleanup stalled sessions. + * + * @param int $lifetime + * The value of session.gc_maxlifetime, passed by PHP. + * Sessions not updated for more than $lifetime seconds will be removed. + */ +function sess_gc($lifetime) { + // 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. + $collection = mongodb_collection(variable_get('mongodb_session', 'session')); + $collection->remove(array('timestamp' => array('$lt' => time() - $lifetime))); + sess_debug('Sess GC: ' . $lifetime); return TRUE; } /** + * Renamed to drupal_save_session(). Stub kept here for API compatibility. + */ +function session_save_session($status = NULL) { + return drupal_save_session($status); +} + +/** * Initialize the session handler, starting a session if needed. */ function drupal_session_initialize() { global $user; - session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection'); + session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write', 'sess_destroy_sid', 'sess_gc'); if (isset($_COOKIE[session_name()])) { // If a session cookie exists, initialize the session. Otherwise the @@ -214,17 +313,13 @@ function drupal_session_initialize() { /** * Forcefully start a session, preserving already set session data. - * - * @ingroup php_wrappers */ 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; @@ -235,26 +330,18 @@ function drupal_session_start() { /** * Commit the current session, if necessary. * - * If an anonymous user already have an empty session, destroy it. + * If an anonymous user already has 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; - } - + sess_debug('Uid: ' . $user->uid, 'Sess Commit user'); 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(); + if (drupal_session_started() && drupal_save_session()) { + // Destroy empty anonymous sessions. + drupal_session_destroy(); } } - else { - // There is session data to store. Start the session if it is not already - // started. + else if (drupal_save_session()) { if (!drupal_session_started()) { drupal_session_start(); } @@ -276,17 +363,19 @@ function drupal_session_started($set = NULL) { /** * Called when an anonymous user becomes authenticated or vice-versa. - * - * @ingroup php_wrappers */ function drupal_session_regenerate() { - global $user, $is_https; - if ($is_https && variable_get('https', FALSE)) { - $insecure_session_name = substr(session_name(), 1); - $params = session_get_cookie_params(); - $session_id = md5(uniqid(mt_rand(), TRUE)); - setcookie($insecure_session_name, $session_id, REQUEST_TIME + $params['lifetime'], $params['path'], $params['domain'], FALSE, $params['httponly']); - $_COOKIE[$insecure_session_name] = $session_id; + global $user; + + // Set the session cookie "httponly" flag to reduce the risk of session + // stealing via XSS. + extract(session_get_cookie_params()); + + if (version_compare(PHP_VERSION, '5.2.0') === 1) { + session_set_cookie_params($lifetime, $path, $domain, $secure, TRUE); + } + else { + session_set_cookie_params($lifetime, $path, $domain, $secure); } if (drupal_session_started()) { @@ -296,94 +385,22 @@ function drupal_session_regenerate() { 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. + // by _sess_read. $account = $user; drupal_session_start(); $user = $account; + sess_debug($user, "Sess Regen: Started new session for user"); } if (isset($old_session_id)) { - $field = $is_https ? 'ssid' : 'sid'; - mongodb_collection(variable_get('mongodb_session', 'session')) - ->update(array('sid' => $old_session_id), array('$set' => array($field => session_id()))); + $collection = mongodb_collection(variable_get('mongodb_session', 'session')); + $new_session_id = session_id(); + $collection->update(array('sid' => $old_session_id), array('sid' => $new_session_id)); + sess_debug("Old sid: " . $old_session_id . "\nNew sid: " . $new_session_id, "Sess Regen"); } } /** - * Session handler assigned by session_set_save_handler(). - * - * Cleanup a specific session. - * - * @param string $sid - * Session ID. - */ -function _drupal_session_destroy($sid) { - global $user, $is_https; - - $field = $is_https ? 'ssid' : 'sid'; - mongodb_collection(variable_get('mongodb_session', 'session')) - ->remove(array($field => $sid)); - - // Reset $_SESSION and $user to prevent a new session from being started - // in drupal_session_commit(). - $_SESSION = array(); - $user = drupal_anonymous_user(); - - // Unset the session cookies. - _drupal_session_delete_cookie(session_name()); - if ($is_https) { - _drupal_session_delete_cookie(substr(session_name(), 1), TRUE); - } -} - -/** - * 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]); - } -} - -/** - * End a specific user's session(s). - * - * @param string $uid - * User ID. - */ -function drupal_session_destroy_uid($uid) { - mongodb_collection(variable_get('mongodb_session', 'session')) - ->remove(array('uid' => $uid)); -} - -/** - * Session handler assigned by session_set_save_handler(). - * - * Cleanup stalled sessions. - * - * @param int $lifetime - * The value of session.gc_maxlifetime, passed by PHP. - * Sessions not updated for more than $lifetime seconds will be removed. - */ -function _drupal_session_garbage_collection($lifetime) { - // 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. - mongodb_collection(variable_get('mongodb_session', 'session')) - ->remove(array('timestamp' => array('$lt' => REQUEST_TIME - $lifetime))); - return TRUE; -} - -/** * Determine whether to save session data of the current request. * * This function allows the caller to temporarily disable writing of @@ -398,9 +415,41 @@ function _drupal_session_garbage_collection($lifetime) { * FALSE if writing session data has been disabled. Otherwise, TRUE. */ function drupal_save_session($status = NULL) { - $save_session = &drupal_static(__FUNCTION__, TRUE); + static $save_session = TRUE; if (isset($status)) { $save_session = $status; } return $save_session; } + +function sess_debug($data, $label = '') { + return; // Comment out this line to log session stuff to /tmp/sess_debug.txt. + + ob_start(); + print_r($data); + print "\n"; + $bt = debug_backtrace(); + foreach ($bt as $trace) { + $file = explode('/', $trace['file']); + print ' - ' . $trace['function'] . ':' . array_pop($file) . ':' . $trace['line'] . "\n"; + } + $string = ob_get_clean(); + $out = "\n-----------------------------------------------------------------\n"; + if ($label) { + $out .= $label .': '. $string; + } + else { + $out .= $string; + } + $out .= "\n"; + + // The temp directory does vary across multiple simpletest instances. + $file = '/tmp/sess_debug.txt'; + if (file_put_contents($file, $out, FILE_APPEND) === FALSE) { + drupal_set_message(t('The file could not be written.'), 'error'); + return FALSE; + } +} + + + diff --git mongodb_session/mongodb_session.info mongodb_session/mongodb_session.info index dd6bd5f..8b3aa66 100644 --- mongodb_session/mongodb_session.info +++ mongodb_session/mongodb_session.info @@ -1,9 +1,8 @@ ; $Id$ name = MongoDB Session -description = Store session information in MongoDB +description = Store session information in MongoDB package = MongoDB -core = 7.x -files[] = mongodb_session.module -files[] = mongodb_session.inc -files[] = mongodb_session.test +core = 6.x + +dependencies[] = mongodb \ No newline at end of file diff --git mongodb_session/mongodb_session.module mongodb_session/mongodb_session.module index f3e7c5b..8565ca0 100644 --- mongodb_session/mongodb_session.module +++ mongodb_session/mongodb_session.module @@ -12,9 +12,6 @@ function mongodb_session_simpletest_alter(&$groups) { // An alternative session handler module would not want to run the original // Session https handling test because it checks the sessions table in the // database. - $groups['MongoDB']['SessionTestCase'] = $groups['Session']['SessionTestCase']; - $groups['MongoDB']['SessionTestCase']['name'] .= ' (MongoDB)'; - unset($groups['Session']); }