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 handbook.', array( + '!message' => '

' . $e->getMessage() . '

', + '@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' => '', '#markup' => t('Back up your database and site before you continue. Learn how.', 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' => '

', - '#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 modules and themes at http://drupal.org.
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 modules and themes on drupal.org. 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' => '

', ); @@ -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' => '

', + '#suffix' => '

', + ); + + $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 handbook.', 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 handbook.', 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 @backends access to your server. See the handbook for other update methods.', + 'Updating modules and themes requires access to your server via one of the following methods: @backends. See the handbook 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 @backends access to your server. See the handbook for other installation methods.', + 'Installing modules and themes requires access to your server via one of the following methods: @backends. See the handbook 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")); } /**