=== modified file 'includes/bootstrap.inc' --- includes/bootstrap.inc 2009-08-24 00:14:18 +0000 +++ includes/bootstrap.inc 2009-08-31 17:40:27 +0000 @@ -509,7 +509,7 @@ function drupal_settings_initialize() { global $base_url, $base_path, $base_root; // Export the following settings.php variables to the global namespace - global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url; + global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $is_https, $base_secure_url, $base_insecure_url; $conf = array(); if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { @@ -519,6 +519,7 @@ function drupal_settings_initialize() { if (isset($base_url)) { // Parse fixed base URL from settings.php. $parts = parse_url($base_url); + $http_protocol = $parts['scheme']; if (!isset($parts['path'])) { $parts['path'] = ''; } @@ -528,9 +529,10 @@ function drupal_settings_initialize() { } else { // Create base URL - $base_root = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http'; + $http_protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http'; + $base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST']; - $base_url = $base_root .= '://' . $_SERVER['HTTP_HOST']; + $base_url = $base_root; // $_SERVER['SCRIPT_NAME'] can, in contrast to $_SERVER['PHP_SELF'], not // be modified by a visitor. @@ -543,6 +545,9 @@ function drupal_settings_initialize() { $base_path = '/'; } } + $is_https = $http_protocol == 'https'; + $base_secure_url = str_replace('http://', 'https://', $base_url); + $base_insecure_url = str_replace('https://', 'http://', $base_url); if ($cookie_domain) { // If the user specifies the cookie domain, also use it for session name. @@ -557,15 +562,6 @@ function drupal_settings_initialize() { $cookie_domain = check_plain($_SERVER['HTTP_HOST']); } } - // To prevent session cookies from being hijacked, a user can configure the - // SSL version of their website to only transfer session cookies via SSL by - // using PHP's session.cookie_secure setting. The browser will then use two - // separate session cookies for the HTTPS and HTTP versions of the site. So we - // must use different session identifiers for HTTPS and HTTP to prevent a - // cookie collision. - if (ini_get('session.cookie_secure')) { - $session_name .= 'SSL'; - } // Strip leading periods, www., and port numbers from cookie domain. $cookie_domain = ltrim($cookie_domain, '.'); if (strpos($cookie_domain, 'www.') === 0) { @@ -578,7 +574,17 @@ function drupal_settings_initialize() { if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) { ini_set('session.cookie_domain', $cookie_domain); } - session_name('SESS' . md5($session_name)); + // To prevent session cookies from being hijacked, a user can configure the + // SSL version of their website to only transfer session cookies via SSL by + // using PHP's session.cookie_secure setting. The browser will then use two + // separate session cookies for the HTTPS and HTTP versions of the site. So we + // must use different session identifiers for HTTPS and HTTP to prevent a + // cookie collision. + if ($is_https) { + ini_set('session.cookie_secure', TRUE); + } + $prefix = ini_get('session.cookie_secure') ? 'SSESS' : 'SESS'; + session_name($prefix . md5($session_name)); } /** === modified file 'includes/common.inc' --- includes/common.inc 2009-08-31 16:46:31 +0000 +++ includes/common.inc 2009-08-31 17:40:27 +0000 @@ -364,6 +364,14 @@ function drupal_goto($path = '', $query extract(parse_url(urldecode($_REQUEST['destination']))); } + $args = array( + 'path' => &$path, + 'query' => &$query, + 'fragment' => &$fragment, + 'http_response_code' => &$http_response_code, + ); + drupal_alter('drupal_goto', $args); + $url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE)); // Allow modules to react to the end of the page request before redirecting. @@ -2088,6 +2096,11 @@ function _format_date_callback(array $ma * - 'language' * An optional language object. Used to build the URL to link to and * look up the proper alias for the link. + * - 'https' + * Whether this URL should point to a secure location. If not specified, + * the current scheme is used, so the user stays on http or https + * respectively. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS + * can only be enforced when the variable 'https' is set to TRUE. * - 'base_url' * Only used internally, to modify the base URL when a language dependent * URL requires so. @@ -2107,6 +2120,7 @@ function url($path = NULL, array $option 'query' => '', 'absolute' => FALSE, 'alias' => FALSE, + 'https' => FALSE, 'prefix' => '' ); if (!isset($options['external'])) { @@ -2144,7 +2158,7 @@ function url($path = NULL, array $option return $path . $options['fragment']; } - global $base_url; + global $base_url, $base_secure_url, $base_insecure_url; $script = &drupal_static(__FUNCTION__); if (!isset($script)) { @@ -2154,9 +2168,21 @@ function url($path = NULL, array $option $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : ''; } + // The base_url might be rewritten from the language rewrite in domain mode. if (!isset($options['base_url'])) { - // The base_url might be rewritten from the language rewrite in domain mode. - $options['base_url'] = $base_url; + if (isset($options['https']) && variable_get('https', FALSE)) { + if ($options['https'] === TRUE) { + $options['base_url'] = $base_secure_url; + $options['absolute'] = TRUE; + } + elseif ($options['https'] === FALSE) { + $options['base_url'] = $base_insecure_url; + $options['absolute'] = TRUE; + } + } + else { + $options['base_url'] = $base_url; + } } // Preserve the original path before aliasing. === modified file 'includes/form.inc' --- includes/form.inc 2009-08-29 16:30:14 +0000 +++ includes/form.inc 2009-08-31 17:40:27 +0000 @@ -979,6 +979,14 @@ function form_builder($form_id, $element // Special handling if we're on the top level form element. if (isset($element['#type']) && $element['#type'] == 'form') { + if (!empty($element['#https']) && variable_get('https', FALSE) && + !menu_path_is_external($element['#action'])) { + global $base_root; + + // Not an external URL so ensure that it is secure. + $element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action']; + } + // Store a complete copy of the form in form_state prior to building the form. $form_state['complete form'] = $element; // Set a flag if we have a correct form submission. This is always TRUE for === modified file 'includes/session.inc' --- includes/session.inc 2009-07-01 12:47:30 +0000 +++ includes/session.inc 2009-08-31 17:40:27 +0000 @@ -66,7 +66,7 @@ function _drupal_session_close() { * was found or the user is anonymous. */ function _drupal_session_read($sid) { - global $user; + global $user, $is_https; // Write and Close handlers are called after destructing objects // since PHP 5.0.5. @@ -76,14 +76,29 @@ function _drupal_session_read($sid) { // Handle the case of first time visitors and clients that don't store // cookies (eg. web crawlers). - if (!isset($_COOKIE[session_name()])) { + $insecure_session_name = substr(session_name(), 1); + if (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name])) { $user = drupal_anonymous_user(); return ''; } // Otherwise, if the session is still active, we have a record of the - // client's session in the database. - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); + // 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. + if ($is_https) { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => $sid))->fetchObject(); + if (!$user) { + if (isset($_COOKIE[$insecure_session_name])) { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array( + ':sid' => $_COOKIE[$insecure_session_name])) + ->fetchObject(); + } + } + } + else { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); + } // We found the client's session record and they are an authenticated user. if ($user && $user->uid > 0) { @@ -122,22 +137,27 @@ function _drupal_session_read($sid) { * This function will always return TRUE. */ function _drupal_session_write($sid, $value) { - global $user; + global $user, $is_https; if (!drupal_save_session()) { // We don't have anything to do if we are not allowed to save the session. return; } + $fields = array( + 'uid' => $user->uid, + 'cache' => isset($user->cache) ? $user->cache : 0, + 'hostname' => ip_address(), + 'session' => $value, + 'timestamp' => REQUEST_TIME, + ); + $insecure_session_name = substr(session_name(), 1); + if ($is_https && isset($_COOKIE[$insecure_session_name])) { + $fields['sid'] = $_COOKIE[$insecure_session_name]; + } db_merge('sessions') - ->key(array('sid' => $sid)) - ->fields(array( - 'uid' => $user->uid, - 'cache' => isset($user->cache) ? $user->cache : 0, - 'hostname' => ip_address(), - 'session' => $value, - 'timestamp' => REQUEST_TIME, - )) + ->key(array($is_https ? 'ssid' : 'sid' => $sid)) + ->fields($fields) ->execute(); // Last access time is updated no more frequently than once every 180 seconds. @@ -246,7 +266,14 @@ function drupal_session_started($set = N * Called when an anonymous user becomes authenticated or vice-versa. */ function drupal_session_regenerate() { - global $user; + 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; + } if (drupal_session_started()) { $old_session_id = session_id(); @@ -264,7 +291,7 @@ function drupal_session_regenerate() { if (isset($old_session_id)) { db_update('sessions') ->fields(array( - 'sid' => session_id() + $is_https ? 'ssid' : 'sid' => session_id() )) ->condition('sid', $old_session_id) ->execute(); @@ -304,11 +331,11 @@ function drupal_session_count($timestamp * Session ID. */ function _drupal_session_destroy($sid) { - global $user; + global $user, $is_https; // Delete session data. db_delete('sessions') - ->condition('sid', $sid) + ->condition($is_https ? 'ssid' : 'sid', $sid) ->execute(); // Reset $_SESSION and $user to prevent a new session from being started @@ -316,11 +343,26 @@ function _drupal_session_destroy($sid) { $_SESSION = array(); $user = drupal_anonymous_user(); - // Unset the session cookie. - if (isset($_COOKIE[session_name()])) { + // 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(session_name(), '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); - unset($_COOKIE[session_name()]); + setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], !$force_insecure && $params['secure'], $params['httponly']); + unset($_COOKIE[$name]); } } === modified file 'modules/simpletest/drupal_web_test_case.php' --- modules/simpletest/drupal_web_test_case.php 2009-08-27 20:25:28 +0000 +++ modules/simpletest/drupal_web_test_case.php 2009-08-31 17:40:27 +0000 @@ -1311,13 +1311,19 @@ class DrupalWebTestCase extends DrupalTe call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1]))); } - // Save the session cookie, if set. - if (preg_match('/^Set-Cookie: ' . preg_quote($this->session_name) . '=([a-z90-9]+)/', $header, $matches)) { - if ($matches[1] != 'deleted') { - $this->session_id = $matches[1]; - } - else { - $this->session_id = NULL; + // Save cookies. + if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) { + $name = $matches[1]; + $parts = array_map('trim', explode(';', $matches[2])); + $value = array_shift($parts); + $this->cookies[$name] = array('value' => $value, 'secure' => in_array('secure', $parts)); + if ($name == $this->session_name) { + if ($value != 'deleted') { + $this->session_id = $value; + } + else { + $this->session_id = NULL; + } } } === modified file 'modules/simpletest/tests/common.test' --- modules/simpletest/tests/common.test 2009-08-25 21:16:31 +0000 +++ modules/simpletest/tests/common.test 2009-08-31 17:40:27 +0000 @@ -601,6 +601,43 @@ class DrupalSetContentTestCase extends D } /** + * Testing drupal_goto and hook_drupal_goto_alter(). + */ +class DrupalGotoTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Drupal goto', + 'description' => 'Performs tests on the drupal_goto function and hook_drupal_goto_alter', + 'group' => 'System' + ); + } + + function setUp() { + parent::setUp('common_test'); + } + + /** + * Test setting and retrieving content for theme regions. + */ + function testDrupalGoto() { + $this->drupalGet('common-test/drupal_goto/redirect'); + + $this->assertNoText(t("Drupal goto failed to stop program"), t("Drupal goto stopped program.")); + $this->assertText('drupal_goto', t("Drupal goto redirect failed.")); + } + + /** + * Test setting and retrieving content for theme regions. + */ + function testDrupalGotoAlter() { + $this->drupalGet('common-test/drupal_goto/redirect_fail'); + + $this->assertNoText(t("Drupal goto failed to stop program"), t("Drupal goto stopped program.")); + $this->assertNoText('drupal_goto_fail', t("Drupal goto redirect failed.")); + } +} + +/** * Tests for the JavaScript system. */ class JavaScriptTestCase extends DrupalWebTestCase { === modified file 'modules/simpletest/tests/common_test.module' --- modules/simpletest/tests/common_test.module 2009-07-04 18:26:42 +0000 +++ modules/simpletest/tests/common_test.module 2009-08-31 17:40:27 +0000 @@ -7,6 +7,70 @@ */ /** + * Implement hook_menu(). + */ +function common_test_menu() { + $items = array(); + $items['common-test/drupal_goto'] = array( + 'title' => 'Drupal Goto', + 'page callback' => 'common_test_drupal_goto_land', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + $items['common-test/drupal_goto/fail'] = array( + 'title' => 'Drupal Goto', + 'page callback' => 'common_test_drupal_goto_land_fail', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + $items['common-test/drupal_goto/redirect'] = array( + 'title' => 'Drupal Goto', + 'page callback' => 'common_test_drupal_goto_redirect', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + $items['common-test/drupal_goto/redirect_fail'] = array( + 'title' => 'Drupal Goto Failure', + 'page callback' => 'drupal_goto', + 'page arguments' => array('common-test/drupal_goto/fail'), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; +} + +/** + * Check that drupal_goto() exits once called. + */ +function common_test_drupal_goto_redirect() { + drupal_goto('common-test/drupal_goto'); + print t("Drupal goto failed to stop program"); +} + +/** + * Landing page for drupal_goto(). + */ +function common_test_drupal_goto_land() { + print "drupal_goto"; +} + +/** + * Fail landing page for drupal_goto(). + */ +function common_test_drupal_goto_land_fail() { + print "drupal_goto_fail"; +} + +/** + * Implement hook_drupal_goto_alter(). + */ +function common_test_drupal_goto_alter(&$args) { + if ($args['path'] == 'common-test/drupal_goto/fail') { + $args['path'] = 'common-test/drupal_goto/redirect'; + } +} + +/** * Implement hook_theme(). */ function common_test_theme() { === modified file 'modules/simpletest/tests/session.test' --- modules/simpletest/tests/session.test 2009-08-17 19:14:39 +0000 +++ modules/simpletest/tests/session.test 2009-08-31 17:40:31 +0000 @@ -250,3 +250,95 @@ class SessionTestCase extends DrupalWebT } } } + +/** + * 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; + + if ($is_https) { + // The functionality does not make sense when running on https. + return; + } + + $insecure_session_name = session_name(); + $secure_session_name = "S$insecure_session_name"; + + // Enable secure pages. + variable_set('https', TRUE); + + $user = $this->drupalCreateUser(array('access administration pages')); + + $this->curlClose(); + $this->drupalGet('session-test/set/1'); + // 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'); + + // 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"]'); + $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'); + $args = array( + ':sid' => $this->cookies[$insecure_session_name]['value'], + ':ssid' => $this->cookies[$secure_session_name]['value'], + ); + $this->assertTrue(db_query('SELECT sid FROM {sessions} WHERE sid = :sid AND ssid = :ssid', $args)->fetchField(), 'Session has both SIDs'); + $cookies = array( + $insecure_session_name . '=' . $args[':sid'], + $secure_session_name . '=' . $args[':ssid'], + ); + + foreach ($cookies as $cookie_key => $cookie) { + foreach (array('admin', $this->httpsUrl('admin')) as $url_key => $url) { + $this->curlClose(); + + $this->drupalGet($url, array(), array('Cookie: ' . $cookie)); + if ($cookie_key == $url_key) { + $this->assertText(t('Administer')); + } + else { + $this->assertNoText(t('Administer')); + } + } + } + } + + protected function httpsUrl($url) { + global $base_url; + return $base_url . '/modules/simpletest/tests/https.php?q=' . $url; + } +} === modified file 'modules/simpletest/tests/session_test.module' --- modules/simpletest/tests/session_test.module 2009-07-01 12:47:30 +0000 +++ modules/simpletest/tests/session_test.module 2009-08-31 17:40:31 +0000 @@ -143,3 +143,20 @@ function session_test_user_login($edit = } } +/** + * Implement hook_form_FORM_ID_alter(). + */ +function session_test_form_user_login_alter(&$form) { + $form['#https'] = TRUE; +} + +/** + * Implement hook_drupal_goto_alter(). + * + * Force the redirection to go to a non-secure page after being on a secure + * page through https.php. + */ +function session_test_drupal_goto_alter(&$args) { + global $base_insecure_url; + $args['path'] = $base_insecure_url . '/' . $args['path']; +} === modified file 'modules/system/system.api.php' --- modules/system/system.api.php 2009-08-31 16:46:31 +0000 +++ modules/system/system.api.php 2009-08-31 17:40:31 +0000 @@ -731,7 +731,7 @@ function hook_system_info_alter(&$info, * Permissions are checked using user_access(). * * For a detailed usage example, see page_example.module. - * + * * @return * An array of which permission names are the keys and their corresponding * values are descriptions of each permission. @@ -2191,7 +2191,27 @@ function hook_profile_tasks() { 'myprofile_final_site_setup' => array( ), ); - return $tasks; + return $tasks; +} + +/** + * Change the page the user is sent to by drupal_goto(). + * + * @param $args + * The array keys are the same as drupal_goto() arguments and the array can + * be changed. + * + * $args = array( + * 'path' => &$path, + * 'query' => &$query, + * 'fragment' => &$fragment, + * 'http_response_code' => &$http_response_code, + * ); + * + */ +function hook_drupal_goto_alter(array $args) { + // A good addition to misery module. + $args['http_response_code'] = 500; } /** === modified file 'modules/system/system.install' --- modules/system/system.install 2009-08-29 10:38:58 +0000 +++ modules/system/system.install 2009-08-31 17:40:31 +0000 @@ -1316,6 +1316,13 @@ function system_schema() { 'not null' => TRUE, 'default' => '', ), + 'ssid' => array( + 'description' => "Unique key: Secure session ID. The value is generated by PHP's Session API.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => FALSE, + 'default' => NULL, + ), 'hostname' => array( 'description' => 'The IP address that last used this session ID (sid).', 'type' => 'varchar', @@ -1347,6 +1354,9 @@ function system_schema() { 'timestamp' => array('timestamp'), 'uid' => array('uid'), ), + 'unique keys' => array( + 'ssid' => array('ssid'), + ), 'foreign keys' => array( 'uid' => array('users' => 'uid'), ),