diff --git a/includes/authorize.inc b/includes/authorize.inc index 5c4d1f0b4b3ea310c49420b647f7b547dfa6750d..4baf2f1523d3b876f60ba757de30f87e74c41123 100644 --- a/includes/authorize.inc +++ b/includes/authorize.inc @@ -194,7 +194,12 @@ function authorize_filetransfer_form_validate($form, &$form_state) { $filetransfer->connect(); } catch (Exception $e) { - form_set_error('connection_settings', $e->getMessage()); + // The format of this error message is similar to that used on the + // database connection form in the installer. + form_set_error('connection_settings', t('Failed to connect to the server. The server reports the following message: !message For more help installing or updating code on your server, see the <a href="@handbook_url">handbook</a>.', array( + '!message' => '<p class="error">' . $e->getMessage() . '</p>', + '@handbook_url' => 'http://drupal.org/documentation/install/modules-themes', + ))); } } } diff --git a/modules/update/tests/update_test.module b/modules/update/tests/update_test.module index 9b8de5b45f25ddc9a85d305e694a2362407ab8a6..fb7d3abfbad7b56c8e14d2c5392a077331c27e57 100644 --- a/modules/update/tests/update_test.module +++ b/modules/update/tests/update_test.module @@ -112,3 +112,40 @@ function update_test_archiver_info() { ), ); } + +/** + * Implements hook_filetransfer_info(). + */ +function update_test_filetransfer_info() { + // Define a mock file transfer method, to ensure that there will always be + // at least one method available in the user interface (regardless of the + // environment in which the update manager tests are run). + return array( + 'system_test' => array( + 'title' => t('Update Test FileTransfer'), + // This should be in an .inc file, but for testing purposes, it is OK to + // leave it in the main module file. + 'file' => 'update_test.module', + 'class' => 'UpdateTestFileTransfer', + 'weight' => -20, + ), + ); +} + +/** + * Mock FileTransfer object to test the settings form functionality. + */ +class UpdateTestFileTransfer { + public static function factory() { + return new UpdateTestFileTransfer; + } + + public function getSettingsForm() { + $form = array(); + $form['udpate_test_username'] = array( + '#type' => 'textfield', + '#title' => t('Update Test Username'), + ); + return $form; + } +} diff --git a/modules/update/update.manager.inc b/modules/update/update.manager.inc index 0e699522a064b96c653925c8b3133764bc7fb7c7..9f0fb8cb72bb16d172d75316b5aef265fd275440 100644 --- a/modules/update/update.manager.inc +++ b/modules/update/update.manager.inc @@ -59,6 +59,10 @@ * The form array for selecting which projects to update. */ function update_manager_update_form($form, $form_state = array(), $context) { + if (!_update_manager_check_backends($form, 'update')) { + return $form; + } + $form['#theme'] = 'update_manager_update_form'; $available = update_get_available(TRUE); @@ -354,6 +358,10 @@ function update_manager_download_batch_finished($success, $results) { * file transfer credentials and attempt to complete the update. */ function update_manager_update_ready_form($form, &$form_state) { + if (!_update_manager_check_backends($form, 'update')) { + return $form; + } + $form['backup'] = array( '#prefix' => '<strong>', '#markup' => t('Back up your database and site before you continue. <a href="@backup_url">Learn how</a>.', array('@backup_url' => url('http://drupal.org/node/22281'))), @@ -461,11 +469,18 @@ function update_manager_update_ready_form_submit($form, &$form_state) { * The form array for selecting which project to install. */ function update_manager_install_form($form, &$form_state, $context) { - $form = array(); + if (!_update_manager_check_backends($form, 'install')) { + return $form; + } $form['help_text'] = array( '#prefix' => '<p>', - '#markup' => t('To install a new module or theme, either enter the URL of an archive file you wish to install, or upload the archive file that you have downloaded. You can find <a href="@module_url">modules</a> and <a href="@theme_url">themes</a> at <a href="@drupal_org_url">http://drupal.org</a>.<br/>The following archive extensions are supported: %extensions.', array('@module_url' => 'http://drupal.org/project/modules', '@theme_url' => 'http://drupal.org/project/themes', '@drupal_org_url' => 'http://drupal.org', '%extensions' => archiver_get_extensions())), + '#markup' => t('You can find <a href="@module_url">modules</a> and <a href="@theme_url">themes</a> on <a href="@drupal_org_url">drupal.org</a>. The following file extensions are supported: %extensions.', array( + '@module_url' => 'http://drupal.org/project/modules', + '@theme_url' => 'http://drupal.org/project/themes', + '@drupal_org_url' => 'http://drupal.org', + '%extensions' => archiver_get_extensions(), + )), '#suffix' => '</p>', ); @@ -496,6 +511,73 @@ function update_manager_install_form($form, &$form_state, $context) { return $form; } +/** + * Checks for file transfer backends and prepares a form fragment about them. + * + * @param array $form + * Reference to the form array we're building. + * @param string $operation + * The Update manager operation we're in the middle of. Can be either + * 'update' or 'install'. Use to provide operation-specific interface text. + * + * @return + * TRUE if the Update manager should continue to the next step in the + * workflow, or FALSE if we've hit a fatal configuration and must halt the + * workflow. + */ +function _update_manager_check_backends(&$form, $operation) { + // If file transfers will be performed locally, we do not need to display any + // warnings or notices to the user and should automatically continue the + // workflow, since we won't be using a FileTransfer backend that requires + // user input or a specific server configuration. + if (update_manager_local_transfers_allowed()) { + return TRUE; + } + + // Otherwise, show the available backends. + $form['available_backends'] = array( + '#prefix' => '<p>', + '#suffix' => '</p>', + ); + + $available_backends = drupal_get_filetransfer_info(); + if (empty($available_backends)) { + if ($operation == 'update') { + $form['available_backends']['#markup'] = t('Your server does not support updating modules and themes from this interface. Instead, update modules and themes by uploading the new versions directly to the server, as described in the <a href="@handbook_url">handbook</a>.', array('@handbook_url' => 'http://drupal.org/getting-started/install-contrib')); + } + else { + $form['available_backends']['#markup'] = t('Your server does not support installing modules and themes from this interface. Instead, install modules and themes by uploading them directly to the server, as described in the <a href="@handbook_url">handbook</a>.', array('@handbook_url' => 'http://drupal.org/getting-started/install-contrib')); + } + return FALSE; + } + + $backend_names = array(); + foreach ($available_backends as $backend) { + $backend_names[] = $backend['title']; + } + if ($operation == 'update') { + $form['available_backends']['#markup'] = format_plural( + count($available_backends), + 'Updating modules and themes requires <strong>@backends access</strong> to your server. See the <a href="@handbook_url">handbook</a> for other update methods.', + 'Updating modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See the <a href="@handbook_url">handbook</a> for other update methods.', + array( + '@backends' => implode(', ', $backend_names), + '@handbook_url' => 'http://drupal.org/getting-started/install-contrib', + )); + } + else { + $form['available_backends']['#markup'] = format_plural( + count($available_backends), + 'Installing modules and themes requires <strong>@backends access</strong> to your server. See the <a href="@handbook_url">handbook</a> for other installation methods.', + 'Installing modules and themes requires access to your server via one of the following methods: <strong>@backends</strong>. See the <a href="@handbook_url">handbook</a> for other installation methods.', + array( + '@backends' => implode(', ', $backend_names), + '@handbook_url' => 'http://drupal.org/getting-started/install-contrib', + )); + } + return TRUE; +} + /** * Validate the form for installing a new project via the update manager. */ @@ -811,6 +893,41 @@ function update_manager_batch_project_get($project, $url, &$context) { $context['finished'] = 1; } +/** + * Determines if file transfers will be performed locally. + * + * If the server is configured such that webserver-created files have the same + * owner as the configuration directory (e.g. sites/default) where new code + * will eventually be installed, the Update manager can transfer files entirely + * locally, without changing their ownership (in other words, without prompting + * the user for FTP, SSH or other credentials). + * + * This server configuration is an inherent security weakness because it allows + * a malicious webserver process to append arbitrary PHP code and then execute + * it. However, it is supported here because it is a common configuration on + * shared hosting, and there is nothing Drupal can do to prevent it. + * + * @return + * TRUE if local file transfers are allowed on this server, or FALSE if not. + * + * @see update_manager_update_ready_form_submit() + * @see update_manager_install_form_submit() + * @see install_check_requirements() + */ +function update_manager_local_transfers_allowed() { + // Compare the owner of a webserver-created temporary file to the owner of + // the configuration directory to determine if local transfers will be + // allowed. + $temporary_file = drupal_tempnam('temporary://', 'update_'); + $local_transfers_allowed = fileowner($temporary_file) === fileowner(conf_path()); + + // Clean up. If this fails, we can ignore it (since this is just a temporary + // file anyway). + @drupal_unlink($temporary_file); + + return $local_transfers_allowed; +} + /** * @} End of "defgroup update_manager_file". */ diff --git a/modules/update/update.test b/modules/update/update.test index 840371552467edf8daae6cbea313aef1cf51c9e1..a1252dcde8e977e4a14a198dd95e60e3e64693b2 100644 --- a/modules/update/update.test +++ b/modules/update/update.test @@ -622,9 +622,9 @@ class UpdateTestUploadCase extends UpdateTestHelper { function testFileNameExtensionMerging() { $this->drupalGet('admin/modules/install'); // Make sure the bogus extension supported by update_test.module is there. - $this->assertPattern('/archive extensions are supported:.*update-test-extension/', t("Found 'update-test-extension' extension")); + $this->assertPattern('/file extensions are supported:.*update-test-extension/', t("Found 'update-test-extension' extension")); // Make sure it didn't clobber the first option from core. - $this->assertPattern('/archive extensions are supported:.*tar/', t("Found 'tar' extension")); + $this->assertPattern('/file extensions are supported:.*tar/', t("Found 'tar' extension")); } /**