Index: UPGRADE.txt =================================================================== RCS file: /cvs/drupal/drupal/UPGRADE.txt,v retrieving revision 1.16 diff -u -p -r1.16 UPGRADE.txt --- UPGRADE.txt 14 Sep 2009 07:33:55 -0000 1.16 +++ UPGRADE.txt 7 Oct 2009 16:57:11 -0000 @@ -35,11 +35,12 @@ Let's begin! More information on multisite configuration is located in INSTALL.txt. -2. If possible, log on as the user with user ID 1, which is the first account - created - also known as the site maintenance account. Only this account will - be able to automatically access update.php in step #10. There are special - instructions in step #10 if you are unable to log on as user ID 1. Do not - close your browser until the final step is complete. +2. If possible, log on either as a user with the "Administer software updates" + permission or as the user with user ID 1, which is the first account + created (also known as the site maintenance account). Only these accounts + will be able to automatically access update.php in step #10. There are + special instructions in step #10 if you are unable to log on as one of + these users. Do not close your browser until the final step is complete. 3. Place the site in "Offline" mode, to let the database updates run without interruption and avoid displaying errors to end users of the site. This Index: update.php =================================================================== RCS file: /cvs/drupal/drupal/update.php,v retrieving revision 1.305 diff -u -p -r1.305 update.php --- update.php 28 Sep 2009 22:16:32 -0000 1.305 +++ update.php 7 Oct 2009 16:57:11 -0000 @@ -13,10 +13,11 @@ define('DRUPAL_ROOT', getcwd()); * Point your browser to "http://www.example.com/update.php" and follow the * instructions. * - * If you are not logged in using the site maintenance account, you will need - * to modify the access check statement inside your settings.php file. After - * finishing the upgrade, be sure to open settings.php again, and change it back - * to its original state! + * If you are not logged in using either the site maintenance account or an + * account with the "Administer software updates" permission, you will need to + * modify the access check statement inside your settings.php file. After + * finishing the upgrade, be sure to open settings.php again, and change it + * back to its original state! */ /** @@ -201,17 +202,43 @@ function update_info_page() { } function update_access_denied_page() { + drupal_add_http_header('403 Forbidden'); + watchdog('access denied', 'update.php', NULL, WATCHDOG_WARNING); drupal_set_title('Access denied'); - return '

Access denied. You are not authorized to access this page. Please log in using the site maintenance account (the account you created during installation). If you cannot log in, you will have to edit settings.php to bypass this access check. To do this:

+ return '

Access denied. You are not authorized to access this page. Please log in using either an account with the administer software updates permission or the site maintenance account (the account you created during installation). If you cannot log in, you will have to edit settings.php to bypass this access check. To do this:

  1. With a text editor find the settings.php file on your system. From the main Drupal directory that you installed all the files into, go to sites/your_site_name if such directory exists, or else to sites/default which applies otherwise.
  2. There is a line inside your settings.php file that says $update_free_access = FALSE;. Change it to $update_free_access = TRUE;.
  3. As soon as the update.php script is done, you must change the settings.php file back to its original form with $update_free_access = FALSE;.
  4. -
  5. To avoid having this problem in the future, remember to log in to your website using the site maintenance account (the account you created during installation) before you backup your database at the beginning of the update process.
  6. +
  7. To avoid having this problem in the future, remember to log in to your website using either an account with the administer software updates permission or the site maintenance account (the account you created during installation) before you backup your database at the beginning of the update process.
'; } /** + * Determines if the current user is allowed to run update.php. + * + * @return + * TRUE if the current user should be granted access, or FALSE otherwise. + */ +function update_access_allowed() { + global $update_free_access, $user; + + // Allow the global variable in settings.php to override the access check. + if (!empty($update_free_access)) { + return TRUE; + } + // Calls to user_access() might fail during the Drupal 6 to 7 update process, + // so we fall back on requiring that the user be logged in as user #1. + try { + require_once drupal_get_path('module', 'user') . '/user.module'; + return user_access('administer software updates'); + } + catch (Exception $e) { + return ($user->uid === 1); + } +} + +/** * Add the update task list to the current page. */ function update_task_list($active = NULL) { @@ -273,13 +300,12 @@ update_prepare_d7_bootstrap(); // Determine if the current user has access to run update.php. drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION); -$update_access_allowed = !empty($update_free_access) || $user->uid == 1; // Only allow the requirements check to proceed if the current user has access // to run updates (since it may expose sensitive information about the site's // configuration). $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; -if (empty($op) && $update_access_allowed) { +if (empty($op) && update_access_allowed()) { require_once DRUPAL_ROOT . '/includes/install.inc'; require_once DRUPAL_ROOT . '/includes/file.inc'; require_once DRUPAL_ROOT . '/modules/system/system.install'; @@ -317,7 +343,7 @@ drupal_maintenance_theme(); ini_set('display_errors', TRUE); // Only proceed with updates if the user is allowed to run them. -if ($update_access_allowed) { +if (update_access_allowed()) { include_once DRUPAL_ROOT . '/includes/install.inc'; include_once DRUPAL_ROOT . '/includes/batch.inc'; Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.155 diff -u -p -r1.155 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 29 Sep 2009 15:31:15 -0000 1.155 +++ modules/simpletest/drupal_web_test_case.php 7 Oct 2009 16:57:18 -0000 @@ -1789,42 +1789,28 @@ class DrupalWebTestCase extends DrupalTe * Takes a path and returns an absolute path. * * @param $path - * The path, can be a Drupal path or a site-relative path. It might have a - * query, too. Can even be an absolute path which is just passed through. + * A path from the internal browser content. * @return - * An absolute path. - * - * @todo What is the intention of this function? It is only invoked from - * locations, where URLs from the *output* are turned into absolute URLs, - * so why do we pass that through url() again? + * The $path with $base_url prepended, if necessary. */ protected function getAbsoluteUrl($path) { + global $base_url, $base_path; + $parts = parse_url($path); - // This is more crude than menu_path_is_external() but enough here. if (empty($parts['host'])) { - $options = array('absolute' => TRUE); - $path = $parts['path']; - $base_path = base_path(); - $n = strlen($base_path); - if (substr($path, 0, $n) == $base_path) { - $path = substr($path, $n); - } - if (isset($parts['query'])) { - parse_str($parts['query'], $options['query']); - // Let's make it a bit more crude. It's not clear why we invoke url() on - // a URL contained in the returned page output again, but since $path is - // FALSE (see $path = $parts['path'] above) with Clean URLs disabled, - // and url() now encodes the passed in query parameter array, we get a - // double-encoded 'q' query string in the resulting absolute URL - // generated by url(). This cannot be avoided in url(). But it could be - // completely avoided if this function wouldn't be calling url(). - // @see SimpleTestURLTestCase->testGetAbsoluteUrl() - if (isset($options['query']['q'])) { - $path = $options['query']['q']; - unset($options['query']['q']); - } + // Ensure that we have a string (and no xpath object). + $path = (string) $path; + // Strip $base_path, if existent. + $length = strlen($base_path); + if (substr($path, 0, $length) === $base_path) { + $path = substr($path, $length); + } + // Ensure that we have an absolute path. + if ($path[0] !== '/') { + $path = '/' . $path; } - $path = url($path, $options); + // Finally, prepend the $base_url. + $path = $base_url . $path; } return $path; } Index: modules/simpletest/simpletest.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.test,v retrieving revision 1.33 diff -u -p -r1.33 simpletest.test --- modules/simpletest/simpletest.test 3 Oct 2009 18:09:32 -0000 1.33 +++ modules/simpletest/simpletest.test 7 Oct 2009 16:57:18 -0000 @@ -293,17 +293,17 @@ class SimpleTestURLTestCase extends Drup $this->drupalGet($url); $absolute = url($url, array('absolute' => TRUE)); $this->assertEqual($absolute, $this->url, t('Passed and requested URL are equal.')); - $this->assertEqual($this->url, $this->getAbsoluteUrl($url), t('Requested and returned absolute URL are equal.')); + $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), t('Requested and returned absolute URL are equal.')); $this->drupalPost(NULL, array(), t('Log in')); $this->assertEqual($absolute, $this->url, t('Passed and requested URL are equal.')); - $this->assertEqual($this->url, $this->getAbsoluteUrl($url), t('Requested and returned absolute URL are equal.')); + $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), t('Requested and returned absolute URL are equal.')); $this->clickLink('Create new account'); $url = 'user/register'; $absolute = url($url, array('absolute' => TRUE)); $this->assertEqual($absolute, $this->url, t('Passed and requested URL are equal.')); - $this->assertEqual($this->url, $this->getAbsoluteUrl($url), t('Requested and returned absolute URL are equal.')); + $this->assertEqual($this->url, $this->getAbsoluteUrl($this->url), t('Requested and returned absolute URL are equal.')); } } Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.801 diff -u -p -r1.801 system.module --- modules/system/system.module 5 Oct 2009 02:43:01 -0000 1.801 +++ modules/system/system.module 7 Oct 2009 16:57:20 -0000 @@ -216,6 +216,10 @@ function system_permission() { 'title' => t('Administer site configuration'), 'description' => t('Configure site-wide settings such as module or theme administration settings.'), ), + 'administer software updates' => array( + 'title' => t('Administer software updates'), + 'description' => t('Run the update.php script.'), + ), 'administer actions' => array( 'title' => t('Administer actions'), 'description' => t('Manage the actions defined for your site.'), Index: modules/system/system.test =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.test,v retrieving revision 1.81 diff -u -p -r1.81 system.test --- modules/system/system.test 3 Oct 2009 19:16:04 -0000 1.81 +++ modules/system/system.test 7 Oct 2009 16:57:21 -0000 @@ -1210,7 +1210,7 @@ class TokenReplaceTestCase extends Drupa // passed properly through the call stack and being handled correctly by a 'known' // token, [node:title]. $this->assertFalse(strcmp($target, $result), t('Basic placeholder tokens replaced.')); - + $raw_tokens = array('title' => '[node:title]'); $generated = token_generate('node', $raw_tokens, array('node' => $node)); $this->assertFalse(strcmp($generated['[node:title]'], check_plain($node->title)), t('Token sanitized.')); @@ -1290,3 +1290,59 @@ array_space[a b] = Value'; $this->assertEqual($parsed, $expected, t('Entire parsed .info string and expected array are identical.')); } } + +/** + * Tests for the update system functionality. + */ +class UpdateScriptFunctionalTest extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Update.php functionality', + 'description' => 'Tests the update script access and functionality.', + 'group' => 'System', + ); + } + + /** + * Tests access to the update script. + */ + function testUpdateAccess() { + global $base_url; + $update_url = $base_url . '/update.php'; + + // Try accessing update.php without the proper permission. + $regular_user = $this->drupalCreateUser(); + $this->drupalLogin($regular_user); + $this->drupalGet($update_url, array('external' => TRUE)); + $this->assertResponse(403); + + // Try accessing update.php as an anonymous user. + $this->drupalLogout(); + $this->drupalGet($update_url, array('external' => TRUE)); + $this->assertResponse(403); + + // Access the update page as user 1. + $user1 = user_load(1); + $user1->pass_raw = user_password(); + require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); + $user1->pass = user_hash_password(trim($user1->pass_raw)); + db_query("UPDATE {users} SET pass = :pass WHERE uid = :uid", array(':pass' => $user1->pass, ':uid' => $user1->uid)); + $this->drupalLogin($user1); + $this->drupalGet($update_url, array('external' => TRUE)); + $this->assertResponse(200); + + // Access the update page with the proper permission. + $update_user = $this->drupalCreateUser(array('administer software updates', 'access administration pages')); + $this->drupalLogin($update_user); + $this->drupalGet($update_url, array('external' => TRUE)); + $this->assertResponse(200); + + // Proceed through the update process without any pending updates. + $this->drupalPost(NULL, array(), t('Continue'), array('external' => TRUE)); + $this->assertText(t('No pending updates.')); + + // Click through back to the administration page. + $this->clickLink('Administration pages'); + $this->assertResponse(200); + } +} Index: modules/update/update.fetch.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/update/update.fetch.inc,v retrieving revision 1.23 diff -u -p -r1.23 update.fetch.inc --- modules/update/update.fetch.inc 5 Oct 2009 01:24:37 -0000 1.23 +++ modules/update/update.fetch.inc 7 Oct 2009 16:57:21 -0000 @@ -149,7 +149,7 @@ function _update_get_fetch_url_base($pro * @see update_requirements() */ function _update_cron_notify() { - include_once DRUPAL_ROOT . '/includes/install.inc'; + module_load_install('update'); $status = update_requirements('runtime'); $params = array(); $notify_all = (variable_get('update_notification_threshold', 'all') == 'all'); Index: modules/update/update.install =================================================================== RCS file: /cvs/drupal/drupal/modules/update/update.install,v retrieving revision 1.10 diff -u -p -r1.10 update.install --- modules/update/update.install 10 Sep 2009 06:38:20 -0000 1.10 +++ modules/update/update.install 7 Oct 2009 16:57:21 -0000 @@ -7,6 +7,57 @@ */ /** + * Implement hook_requirements(). + * + * @return + * An array describing the status of the site regarding available updates. + * If there is no update data, only one record will be returned, indicating + * that the status of core can't be determined. If data is available, there + * will be two records: one for core, and another for all of contrib + * (assuming there are any contributed modules or themes enabled on the + * site). In addition to the fields expected by hook_requirements ('value', + * 'severity', and optionally 'description'), this array will contain a + * 'reason' attribute, which is an integer constant to indicate why the + * given status is being returned (UPDATE_NOT_SECURE, UPDATE_NOT_CURRENT, or + * UPDATE_UNKNOWN). This is used for generating the appropriate e-mail + * notification messages during update_cron(), and might be useful for other + * modules that invoke update_requirements() to find out if the site is up + * to date or not. + * + * @see _update_message_text() + * @see _update_cron_notify() + */ +function update_requirements($phase) { + if ($phase == 'runtime') { + if ($available = update_get_available(FALSE)) { + module_load_include('inc', 'update', 'update.compare'); + $data = update_calculate_project_data($available); + // First, populate the requirements for core: + $requirements['update_core'] = _update_requirement_check($data['drupal'], 'core'); + // We don't want to check drupal a second time. + unset($data['drupal']); + if (!empty($data)) { + // Now, sort our $data array based on each project's status. The + // status constants are numbered in the right order of precedence, so + // we just need to make sure the projects are sorted in ascending + // order of status, and we can look at the first project we find. + uasort($data, '_update_project_status_sort'); + $first_project = reset($data); + $requirements['update_contrib'] = _update_requirement_check($first_project, 'contrib'); + } + } + else { + $requirements['update_core']['title'] = t('Drupal core update status'); + $requirements['update_core']['value'] = t('No update data available'); + $requirements['update_core']['severity'] = REQUIREMENT_WARNING; + $requirements['update_core']['reason'] = UPDATE_UNKNOWN; + $requirements['update_core']['description'] = _update_no_data(); + } + return $requirements; + } +} + +/** * Implement hook_uninstall(). */ function update_uninstall() { @@ -21,7 +72,6 @@ function update_uninstall() { foreach ($variables as $variable) { variable_del($variable); } - menu_rebuild(); } /** Index: modules/update/update.module =================================================================== RCS file: /cvs/drupal/drupal/modules/update/update.module,v retrieving revision 1.47 diff -u -p -r1.47 update.module --- modules/update/update.module 10 Sep 2009 06:32:54 -0000 1.47 +++ modules/update/update.module 7 Oct 2009 16:57:21 -0000 @@ -74,7 +74,7 @@ function update_help($path, $arg) { return $output; case 'admin/appearance': case 'admin/config/modules': - include_once DRUPAL_ROOT . '/includes/install.inc'; + module_load_install('update'); $status = update_requirements('runtime'); foreach (array('core', 'contrib') as $report_type) { $type = 'update_' . $report_type; @@ -104,8 +104,8 @@ function update_help($path, $arg) { // Otherwise, if we're on *any* admin page and there's a security // update missing, print an error message about it. if (arg(0) == 'admin' && strpos($path, '#') === FALSE - && user_access('administer site configuration')) { - include_once DRUPAL_ROOT . '/includes/install.inc'; + && user_access('administer software updates')) { + module_load_install('update'); $status = update_requirements('runtime'); foreach (array('core', 'contrib') as $report_type) { $type = 'update_' . $report_type; @@ -116,7 +116,6 @@ function update_help($path, $arg) { } } } - } } @@ -130,27 +129,26 @@ function update_menu() { 'title' => 'Available updates', 'description' => 'Get a status report about available updates for your installed modules and themes.', 'page callback' => 'update_status', - 'access arguments' => array('administer site configuration'), + 'access arguments' => array('administer software updates'), 'weight' => 10, 'file' => 'update.report.inc', ); $items['admin/reports/updates/list'] = array( 'title' => 'List', - 'access arguments' => array('administer site configuration'), 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/reports/updates/settings'] = array( 'title' => 'Settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('update_settings'), - 'access arguments' => array('administer site configuration'), + 'access arguments' => array('administer software updates'), 'file' => 'update.settings.inc', 'type' => MENU_LOCAL_TASK, ); $items['admin/reports/updates/check'] = array( 'title' => 'Manual update check', 'page callback' => 'update_manual_status', - 'access arguments' => array('administer site configuration'), + 'access arguments' => array('administer software updates'), 'type' => MENU_CALLBACK, 'file' => 'update.fetch.inc', ); @@ -176,57 +174,6 @@ function update_theme() { } /** - * Implement hook_requirements(). - * - * @return - * An array describing the status of the site regarding available updates. - * If there is no update data, only one record will be returned, indicating - * that the status of core can't be determined. If data is available, there - * will be two records: one for core, and another for all of contrib - * (assuming there are any contributed modules or themes enabled on the - * site). In addition to the fields expected by hook_requirements ('value', - * 'severity', and optionally 'description'), this array will contain a - * 'reason' attribute, which is an integer constant to indicate why the - * given status is being returned (UPDATE_NOT_SECURE, UPDATE_NOT_CURRENT, or - * UPDATE_UNKNOWN). This is used for generating the appropriate e-mail - * notification messages during update_cron(), and might be useful for other - * modules that invoke update_requirements() to find out if the site is up - * to date or not. - * - * @see _update_message_text() - * @see _update_cron_notify() - */ -function update_requirements($phase) { - if ($phase == 'runtime') { - if ($available = update_get_available(FALSE)) { - module_load_include('inc', 'update', 'update.compare'); - $data = update_calculate_project_data($available); - // First, populate the requirements for core: - $requirements['update_core'] = _update_requirement_check($data['drupal'], 'core'); - // We don't want to check drupal a second time. - unset($data['drupal']); - if (!empty($data)) { - // Now, sort our $data array based on each project's status. The - // status constants are numbered in the right order of precedence, so - // we just need to make sure the projects are sorted in ascending - // order of status, and we can look at the first project we find. - uasort($data, '_update_project_status_sort'); - $first_project = reset($data); - $requirements['update_contrib'] = _update_requirement_check($first_project, 'contrib'); - } - } - else { - $requirements['update_core']['title'] = t('Drupal core update status'); - $requirements['update_core']['value'] = t('No update data available'); - $requirements['update_core']['severity'] = REQUIREMENT_WARNING; - $requirements['update_core']['reason'] = UPDATE_UNKNOWN; - $requirements['update_core']['description'] = _update_no_data(); - } - return $requirements; - } -} - -/** * Private helper method to fill in the requirements array. * * This is shared for both core and contrib to generate the right elements in Index: modules/update/update.test =================================================================== RCS file: /cvs/drupal/drupal/modules/update/update.test,v retrieving revision 1.4 diff -u -p -r1.4 update.test --- modules/update/update.test 5 Oct 2009 02:26:36 -0000 1.4 +++ modules/update/update.test 7 Oct 2009 16:57:21 -0000 @@ -64,7 +64,7 @@ class UpdateCoreTestCase extends UpdateT function setUp() { parent::setUp('update_test', 'update'); - $admin_user = $this->drupalCreateUser(array('administer site configuration')); + $admin_user = $this->drupalCreateUser(array('administer software updates')); $this->drupalLogin($admin_user); } @@ -160,7 +160,7 @@ class UpdateTestContribCase extends Upda function setUp() { parent::setUp('update_test', 'update', 'aaa_update_test', 'bbb_update_test', 'ccc_update_test'); - $admin_user = $this->drupalCreateUser(array('administer site configuration')); + $admin_user = $this->drupalCreateUser(array('administer software updates')); $this->drupalLogin($admin_user); } @@ -267,4 +267,3 @@ class UpdateTestContribCase extends Upda } } - Index: sites/default/default.settings.php =================================================================== RCS file: /cvs/drupal/drupal/sites/default/default.settings.php,v retrieving revision 1.31 diff -u -p -r1.31 default.settings.php --- sites/default/default.settings.php 14 Sep 2009 19:03:04 -0000 1.31 +++ sites/default/default.settings.php 7 Oct 2009 16:57:21 -0000 @@ -157,7 +157,8 @@ $db_prefix = ''; * Access control for update.php script * * If you are updating your Drupal installation using the update.php script but - * are not logged in using the site maintenance account (the account that was + * are not logged in using either an account with the "Administer software + * updates" permission or the site maintenance account (the account that was * created during installation), you will need to modify the access check * statement below. Change the FALSE to a TRUE to disable the access check. * After finishing the upgrade, be sure to open this file again and change the