diff --git a/core/modules/language/tests/language_elements_test/language_elements_test.module b/core/modules/language/tests/language_elements_test/language_elements_test.module deleted file mode 100644 index a85b7f3..0000000 --- a/core/modules/language/tests/language_elements_test/language_elements_test.module +++ /dev/null @@ -1,41 +0,0 @@ - 'language_configuration', - '#entity_information' => array( - 'entity_type' => 'some_custom_type', - 'bundle' => 'some_bundle', - ), - '#default_value' => $conf, - ); - - $form['submit'] = array( - '#type' => 'submit', - '#value' => 'Save', - ); - $form['#submit'][] = 'language_configuration_element_submit'; - return $form; -} - -/** - * A form containing a language select element. - */ -function language_elements_configuration_element_test() { - $form['langcode'] = array( - '#title' => t('Language select'), - '#type' => 'language_select', - '#default_value' => language_get_default_langcode('custom_type', 'some_bundle'), - ); - return $form; -} diff --git a/core/modules/language/tests/language_elements_test/language_elements_test.routing.yml b/core/modules/language/tests/language_elements_test/language_elements_test.routing.yml index 5a5698c..168bfbe 100644 --- a/core/modules/language/tests/language_elements_test/language_elements_test.routing.yml +++ b/core/modules/language/tests/language_elements_test/language_elements_test.routing.yml @@ -1,7 +1,7 @@ language_elements_test.config_element: path: '/language-tests/language_configuration_element' defaults: - _content: '\Drupal\language_elements_test\Form\LanguageElementsTestForm::configFormElement' + _form: '\Drupal\language_elements_test\Form\LanguageConfigurationElement' _title: 'Language configuration form element' requirements: _access: 'TRUE' @@ -9,7 +9,7 @@ language_elements_test.config_element: language_elements_test.config_element_test: path: '/language-tests/language_configuration_element_test' defaults: - _content: '\Drupal\language_elements_test\Form\LanguageElementsTestForm::configFormElementTest' + _form: '\Drupal\language_elements_test\Form\LanguageConfigurationElementTest' _title: 'Language configuration form element' requirements: _access: 'TRUE' diff --git a/core/modules/language/tests/language_elements_test/src/Form/LanguageConfigurationElement.php b/core/modules/language/tests/language_elements_test/src/Form/LanguageConfigurationElement.php new file mode 100644 index 0000000..58d2d0d --- /dev/null +++ b/core/modules/language/tests/language_elements_test/src/Form/LanguageConfigurationElement.php @@ -0,0 +1,51 @@ + 'language_configuration', + '#entity_information' => array( + 'entity_type' => 'some_custom_type', + 'bundle' => 'some_bundle', + ), + '#default_value' => $conf, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => 'Save', + ); + $form['#submit'][] = 'language_configuration_element_submit'; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + } +} diff --git a/core/modules/language/tests/language_elements_test/src/Form/LanguageConfigurationElementTest.php b/core/modules/language/tests/language_elements_test/src/Form/LanguageConfigurationElementTest.php new file mode 100644 index 0000000..274c56b --- /dev/null +++ b/core/modules/language/tests/language_elements_test/src/Form/LanguageConfigurationElementTest.php @@ -0,0 +1,40 @@ + t('Language select'), + '#type' => 'language_select', + '#default_value' => language_get_default_langcode('custom_type', 'some_bundle'), + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + } +} diff --git a/core/modules/language/tests/language_elements_test/src/Form/LanguageElementsTestForm.php b/core/modules/language/tests/language_elements_test/src/Form/LanguageElementsTestForm.php deleted file mode 100644 index 010b472..0000000 --- a/core/modules/language/tests/language_elements_test/src/Form/LanguageElementsTestForm.php +++ /dev/null @@ -1,34 +0,0 @@ -getForm('language_elements_configuration_element'); - } - - /** - * Wraps language_element_tests_configuration_element_test(). - * - * @todo Remove language_element_tests_configuration_element_test(). - */ - public function configFormElementTest() { - return \Drupal::formBuilder()->getForm('language_elements_configuration_element_test'); - } - -} - diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index ea0ec44..a0efd9f 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -1369,3 +1369,19 @@ function _locale_rebuild_js($langcode = NULL) { return TRUE; } } +/** + * Form element callback: After build changes to the language update table. + * + * Adds labels to the languages and removes checkboxes from languages from which + * translation files could not be found. + */ +function locale_translation_language_table($form_element) { + // Remove checkboxes of languages without updates. + if ($form_element['#not_found']) { + foreach ($form_element['#not_found'] as $langcode) { + $form_element[$langcode] = array(); + } + } + return $form_element; +} + diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index 8a07c4c..68870ee 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -37,234 +37,6 @@ function locale_translation_manual_status() { } /** - * Page callback: Display the current translation status. - * - * @see locale_menu() - * - * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. - * Use \Drupal\locale\Form\LocaleForm::status(). - */ -function locale_translation_status_form($form, &$form_state) { - module_load_include('translation.inc', 'locale'); - module_load_include('compare.inc', 'locale'); - $updates = $options = array(); - $languages_update = $languages_not_found = array(); - $projects_update = array(); - - // @todo Calling locale_translation_build_projects() is an expensive way to - // get a module name. In follow-up issue http://drupal.org/node/1842362 - // the project name will be stored to display use, like here. - $project_data = locale_translation_build_projects(); - $languages = locale_translatable_language_list(); - $status = locale_translation_get_status(); - - // Prepare information about projects which have available translation - // updates. - if ($languages && $status) { - foreach ($status as $project) { - foreach ($project as $langcode => $project_info) { - // No translation file found for this project-language combination. - if (empty($project_info->type)) { - $updates[$langcode]['not_found'][] = array( - 'name' => $project_info->name == 'drupal' ? t('Drupal core') : $project_data[$project_info->name]->info['name'], - 'version' => $project_info->version, - 'info' => _locale_translation_status_debug_info($project_info), - ); - $languages_not_found[$langcode] = $langcode; - } - // Translation update found for this project-language combination. - elseif ($project_info->type == LOCALE_TRANSLATION_LOCAL || $project_info->type == LOCALE_TRANSLATION_REMOTE ) { - $local = isset($project_info->files[LOCALE_TRANSLATION_LOCAL]) ? $project_info->files[LOCALE_TRANSLATION_LOCAL] : NULL; - $remote = isset($project_info->files[LOCALE_TRANSLATION_REMOTE]) ? $project_info->files[LOCALE_TRANSLATION_REMOTE] : NULL; - $recent = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local; - $updates[$langcode]['updates'][] = array( - 'name' => $project_data[$project_info->name]->info['name'], - 'version' => $project_info->version, - 'timestamp' => $recent->timestamp, - ); - $languages_update[$langcode] = $langcode; - $projects_update[$project_info->name] = $project_info->name; - } - } - } - $languages_not_found = array_diff($languages_not_found, $languages_update); - - // Build data options for the select table. - foreach($updates as $langcode => $update) { - $title = String::checkPlain($languages[$langcode]->name); - $locale_translation_update_info = array('#theme' => 'locale_translation_update_info'); - foreach (array('updates', 'not_found') as $update_status) { - if (isset($update[$update_status])) { - $locale_translation_update_info['#' . $update_status] = $update[$update_status]; - } - } - $options[$langcode] = array( - 'title' => array( - 'class' => array('label'), - 'data' => array( - '#title' => $title, - '#markup' => $title - ), - ), - 'status' => array('class' => array('description', 'expand', 'priority-low'), 'data' => drupal_render($locale_translation_update_info)), - ); - } - // Sort the table data on language name. - uasort($options, function ($a, $b) { - return strcasecmp($a['title']['data']['#title'], $b['title']['data']['#title']); - }); - } - - $last_checked = \Drupal::state()->get('locale.translation_last_checked'); - $form['last_checked'] = array( - '#theme' => 'locale_translation_last_check', - '#last' => $last_checked, - ); - - $header = array( - 'title' => array( - 'data' => t('Language'), - 'class' => array('title'), - ), - 'status' => array( - 'data' => t('Status'), - 'class' => array('status', 'priority-low'), - ), - ); - - if (!$languages) { - $empty = t('No translatable languages available. Add a language first.', array('@add_language' => url('admin/config/regional/language'))); - } - elseif ($status) { - $empty = t('All translations up to date.'); - } - else { - $empty = t('No translation status available. Check manually.', array('@check' => url('admin/reports/translations/check'))); - } - - // The projects which require an update. Used by the _submit callback. - $form['projects_update'] = array( - '#type' => 'value', - '#value' => $projects_update, - ); - - $form['langcodes'] = array( - '#type' => 'tableselect', - '#header' => $header, - '#options' => $options, - '#default_value' => $languages_update, - '#empty' => $empty, - '#js_select' => TRUE, - '#multiple' => TRUE, - '#required' => TRUE, - '#not_found' => $languages_not_found, - '#after_build' => array('locale_translation_language_table'), - ); - - $form['#attached']['library'][] = 'locale/drupal.locale.admin'; - - $form['actions'] = array('#type' => 'actions'); - if ($languages_update) { - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Update translations'), - ); - } - - return $form; -} - -/** - * Form validation handler for locale_translation_status_form(). - */ -function locale_translation_status_form_validate($form, &$form_state) { - // Check if a language has been selected. 'tableselect' doesn't. - if (!array_filter($form_state['values']['langcodes'])) { - form_set_error('', $form_state, t('Select a language to update.')); - } -} - -/** - * Form submission handler for locale_translation_status_form(). - */ -function locale_translation_status_form_submit($form, &$form_state) { - module_load_include('fetch.inc', 'locale'); - $langcodes = array_filter($form_state['values']['langcodes']); - $projects = array_filter($form_state['values']['projects_update']); - - // Set the translation import options. This determines if existing - // translations will be overwritten by imported strings. - $options = _locale_translation_default_update_options(); - - // If the status was updated recently we can immediately start fetching the - // translation updates. If the status is expired we clear it an run a batch to - // update the status and then fetch the translation updates. - $last_checked = \Drupal::state()->get('locale.translation_last_checked'); - if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) { - locale_translation_clear_status(); - $batch = locale_translation_batch_update_build(array(), $langcodes, $options); - batch_set($batch); - } - else { - $batch = locale_translation_batch_fetch_build($projects, $langcodes, $options); - batch_set($batch); - } -} - -/** - * Form element callback: After build changes to the language update table. - * - * Adds labels to the languages and removes checkboxes from languages from which - * translation files could not be found. - */ -function locale_translation_language_table($form_element) { - // Remove checkboxes of languages without updates. - if ($form_element['#not_found']) { - foreach ($form_element['#not_found'] as $langcode) { - $form_element[$langcode] = array(); - } - } - return $form_element; -} - -/** - * Provides debug info for projects in case translation files are not found. - * - * Translations files are being fetched either from Drupal translation server - * and local files or only from the local filesystem depending on the - * "Translation source" setting at admin/config/regional/translate/settings. - * This method will produce debug information including the respective path(s) - * based on this setting. - * - * Translations for development versions are never fetched, so the debug info - * for that is a fixed message. - * - * @param array $source - * An array which is the project information of the source. - * - * @return string - * The string which contains debug information. - */ -function _locale_translation_status_debug_info($source) { - $remote_path = isset($source->files['remote']->uri) ? $source->files['remote']->uri : ''; - $local_path = isset($source->files['local']->uri) ? $source->files['local']->uri : ''; - - if (strpos($source->version, 'dev') !== FALSE) { - return t('No translation files are provided for development releases.'); - } - if (locale_translation_use_remote_source() && $remote_path && $local_path) { - return t('File not found at %remote_path nor at %local_path', array( - '%remote_path' => $remote_path, - '%local_path' => $local_path, - )); - } - elseif ($local_path) { - return t('File not found at %local_path', array('%local_path' => $local_path)); - } - return t('Translation file location could not be determined.'); -} - -/** * Returns HTML for translation edit form. * * @param array $variables @@ -320,8 +92,6 @@ function theme_locale_translate_edit_form_strings($variables) { * An associative array containing: * - updates: The projects which have updates. * - not_found: The projects which updates are not found. - * - * @see locale_translation_status_form() */ function template_preprocess_locale_translation_update_info(&$variables) { $details = array(); @@ -378,8 +148,6 @@ function template_preprocess_locale_translation_update_info(&$variables) { * @param $variables * An associative array containing: * - last: The timestamp when the site last checked for available updates. - * - * @see locale_translation_status_form() */ function template_preprocess_locale_translation_last_check(&$variables) { $last = $variables['last']; diff --git a/core/modules/locale/locale.routing.yml b/core/modules/locale/locale.routing.yml index a5703d8..9c3936c 100644 --- a/core/modules/locale/locale.routing.yml +++ b/core/modules/locale/locale.routing.yml @@ -40,7 +40,7 @@ locale.translate_export: locale.translate_status: path: '/admin/reports/translations' defaults: - _content: '\Drupal\locale\Form\LocaleForm::status' + _form: '\Drupal\locale\Form\TranslationStatusForm' _title: 'Available translation updates' requirements: _permission: 'translate interface' diff --git a/core/modules/locale/src/Form/LocaleForm.php b/core/modules/locale/src/Form/LocaleForm.php deleted file mode 100644 index 280f1b0..0000000 --- a/core/modules/locale/src/Form/LocaleForm.php +++ /dev/null @@ -1,24 +0,0 @@ -getForm('locale_translation_status_form'); - } - -} diff --git a/core/modules/locale/src/Form/TranslationStatusForm.php b/core/modules/locale/src/Form/TranslationStatusForm.php new file mode 100644 index 0000000..74bff5c --- /dev/null +++ b/core/modules/locale/src/Form/TranslationStatusForm.php @@ -0,0 +1,301 @@ +get('module_handler'), + $container->get('state') + ); + } + + /** + * Constructs a TranslationStatusForm object. + * + * @param ModuleHandlerInterface $module_handler + * A module handler. + * @param \Drupal\Core\State\StateInterface $state + * The state service. + */ + public function __construct(ModuleHandlerInterface $module_handler, StateInterface $state) { + $this->moduleHandler = $module_handler; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'locale_translation_status_form'; + } + + /** + * Form builder for displaying the current translation status. + * + * @ingroup forms + */ + public function buildForm(array $form, array &$form_state) { + $languages = locale_translatable_language_list(); + $status = locale_translation_get_status(); + $options = array(); + $languages_update = array(); + $languages_not_found = array(); + $projects_update = array(); + // Prepare information about projects which have available translation + // updates. + if ($languages && $status) { + $updates = $this->prepareUpdateData($status); + + // Build data options for the select table. + foreach ($updates as $langcode => $update) { + $title = String::checkPlain($languages[$langcode]->name); + $locale_translation_update_info = array('#theme' => 'locale_translation_update_info'); + foreach (array('updates', 'not_found') as $update_status) { + if (isset($update[$update_status])) { + $locale_translation_update_info['#' . $update_status] = $update[$update_status]; + } + } + $options[$langcode] = array( + 'title' => array( + 'class' => array('label'), + 'data' => array( + '#title' => $title, + '#markup' => $title, + ), + ), + 'status' => array( + 'class' => array('description', 'expand', 'priority-low'), + 'data' => drupal_render($locale_translation_update_info), + ), + ); + if (!empty($update['not_found'])) { + $languages_not_found[$langcode] = $langcode; + } + elseif (!empty($update['updates'])) { + $languages_update[$langcode] = $langcode; + } + } + // Sort the table data on language name. + uasort($options, function ($a, $b) { + return strcasecmp($a['title']['data']['#title'], $b['title']['data']['#title']); + }); + $languages_not_found = array_diff($languages_not_found, $languages_update); + } + + $last_checked = $this->state->get('locale.translation_last_checked'); + $form['last_checked'] = array( + '#theme' => 'locale_translation_last_check', + '#last' => $last_checked, + ); + + $header = array( + 'title' => array( + 'data' => $this->t('Language'), + 'class' => array('title'), + ), + 'status' => array( + 'data' => $this->t('Status'), + 'class' => array('status', 'priority-low'), + ), + ); + + if (!$languages) { + $empty = $this->t('No translatable languages available. Add a language first.', array( + '@add_language' => $this->url('language.admin_overview'), + )); + } + elseif ($status) { + $empty = $this->t('All translations up to date.'); + } + else { + $empty = $this->t('No translation status available. Check manually.', array( + '@check' => $this->url('locale.check_translation'), + )); + } + + // The projects which require an update. Used by the _submit callback. + $form['projects_update'] = array( + '#type' => 'value', + '#value' => $projects_update, + ); + + $form['langcodes'] = array( + '#type' => 'tableselect', + '#header' => $header, + '#options' => $options, + '#default_value' => $languages_update, + '#empty' => $empty, + '#js_select' => TRUE, + '#multiple' => TRUE, + '#required' => TRUE, + '#not_found' => $languages_not_found, + '#after_build' => array('locale_translation_language_table'), + ); + + $form['#attached']['library'][] = 'locale/drupal.locale.admin'; + $form['#attached']['css'] = array(drupal_get_path('module', 'locale') . '/css/locale.admin.css'); + + $form['actions'] = array('#type' => 'actions'); + if ($languages_update) { + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Update translations'), + ); + } + + return $form; + } + + /** + * Prepare information about projects with available translation updates. + * + * @param array $status + * Translation update status as an array keyed by Project ID and langcode. + * + * @return array + * Translation update status as an array keyed by language code and + * translation update status. + */ + protected function prepareUpdateData(array $status) { + $updates = array(); + + // @todo Calling locale_translation_build_projects() is an expensive way to + // get a module name. In follow-up issue http://drupal.org/node/1842362 + // the project name will be stored to display use, like here. + $this->moduleHandler->loadInclude('locale', 'compare.inc'); + $project_data = locale_translation_build_projects(); + + foreach ($status as $project_id => $project) { + foreach ($project as $langcode => $project_info) { + // No translation file found for this project-language combination. + if (empty($project_info->type)) { + $updates[$langcode]['not_found'][] = array( + 'name' => $project_info->name == 'drupal' ? $this->t('Drupal core') : $project_data[$project_info->name]->info['name'], + 'version' => $project_info->version, + 'info' => $this->createInfoString($project_info), + ); + } + // Translation update found for this project-language combination. + elseif ($project_info->type == LOCALE_TRANSLATION_LOCAL || $project_info->type == LOCALE_TRANSLATION_REMOTE) { + $local = isset($project_info->files[LOCALE_TRANSLATION_LOCAL]) ? $project_info->files[LOCALE_TRANSLATION_LOCAL] : NULL; + $remote = isset($project_info->files[LOCALE_TRANSLATION_REMOTE]) ? $project_info->files[LOCALE_TRANSLATION_REMOTE] : NULL; + $recent = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local; + $updates[$langcode]['updates'][] = array( + 'name' => $project_data[$project_info->name]->info['name'], + 'version' => $project_info->version, + 'timestamp' => $recent->timestamp, + ); + } + } + } + return $updates; + } + + /** + * Provides debug info for projects in case translation files are not found. + * + * Translations files are being fetched either from Drupal translation server + * and local files or only from the local filesystem depending on the + * "Translation source" setting at admin/config/regional/translate/settings. + * This method will produce debug information including the respective path(s) + * based on this setting. + * + * Translations for development versions are never fetched, so the debug info + * for that is a fixed message. + * + * @param array $project_info + * An array which is the project information of the source. + * + * @return string + * The string which contains debug information. + */ + protected function createInfoString($project_info) { + $remote_path = isset($project_info->files['remote']->uri) ? $project_info->files['remote']->uri : FALSE; + $local_path = isset($project_info->files['local']->uri) ? $project_info->files['local']->uri : FALSE; + + if (strpos($project_info->version, 'dev') !== FALSE) { + return $this->t('No translation files are provided for development releases.'); + } + if (locale_translation_use_remote_source() && $remote_path && $local_path) { + return $this->t('File not found at %remote_path nor at %local_path', array( + '%remote_path' => $remote_path, + '%local_path' => $local_path, + )); + } + elseif ($local_path) { + return $this->t('File not found at %local_path', array('%local_path' => $local_path)); + } + return $this->t('Translation file location could not be determined.'); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + // Check if a language has been selected. 'tableselect' doesn't. + if (!array_filter($form_state['values']['langcodes'])) { + form_set_error('', $this->t('Select a language to update.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $this->moduleHandler->loadInclude('locale', 'fetch.inc'); + $langcodes = array_filter($form_state['values']['langcodes']); + $projects = array_filter($form_state['values']['projects_update']); + + // Set the translation import options. This determines if existing + // translations will be overwritten by imported strings. + $options = _locale_translation_default_update_options(); + + // If the status was updated recently we can immediately start fetching the + // translation updates. If the status is expired we clear it an run a batch to + // update the status and then fetch the translation updates. + $last_checked = $this->state->get('locale.translation_last_checked'); + if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) { + locale_translation_clear_status(); + $batch = locale_translation_batch_update_build(array(), $langcodes, $options); + batch_set($batch); + } + else { + $batch = locale_translation_batch_fetch_build($projects, $langcodes, $options); + batch_set($batch); + } + } + +} diff --git a/core/modules/locale/src/Tests/LocaleUpdateTest.php b/core/modules/locale/src/Tests/LocaleUpdateTest.php index 661c77b..9a5dffc 100644 --- a/core/modules/locale/src/Tests/LocaleUpdateTest.php +++ b/core/modules/locale/src/Tests/LocaleUpdateTest.php @@ -70,6 +70,10 @@ function testUpdateProjects() { * for local files only, check for both local and remote files. */ function testUpdateCheckStatus() { + // Case when contributed modules are absent. + $this->drupalGet('admin/reports/translations'); + $this->assertText(t('Missing translations for one project')); + $config = \Drupal::config('locale.settings'); // Set a flag to let the locale_test module replace the project data with a // set of test projects. diff --git a/core/modules/update/src/Form/UpdateForm.php b/core/modules/update/src/Form/UpdateForm.php deleted file mode 100644 index 8f416c1..0000000 --- a/core/modules/update/src/Form/UpdateForm.php +++ /dev/null @@ -1,84 +0,0 @@ -getForm('update_manager_install_form', 'report'); - } - - /** - * Wraps update_manager_update_form(). - * - * @todo Remove update_manager_update_form(). - */ - public function reportUpdate() { - module_load_include('manager.inc', 'update'); - return \Drupal::formBuilder()->getForm('update_manager_update_form', 'report'); - } - - /** - * Wraps update_manager_install_form(). - * - * @todo Remove update_manager_install_form(). - */ - public function moduleInstall() { - module_load_include('manager.inc', 'update'); - return \Drupal::formBuilder()->getForm('update_manager_install_form', 'module'); - } - - /** - * Wraps update_manager_update_form(). - * - * @todo Remove update_manager_update_form(). - */ - public function moduleUpdate() { - module_load_include('manager.inc', 'update'); - return \Drupal::formBuilder()->getForm('update_manager_update_form', 'module'); - } - - /** - * Wraps update_manager_install_form(). - * - * @todo Remove update_manager_install_form(). - */ - public function themeInstall() { - module_load_include('manager.inc', 'update'); - return \Drupal::formBuilder()->getForm('update_manager_install_form', 'theme'); - } - - /** - * Wraps update_manager_update_form(). - * - * @todo Remove update_manager_update_form(). - */ - public function themeUpdate() { - module_load_include('manager.inc', 'update'); - return \Drupal::formBuilder()->getForm('update_manager_update_form', 'theme'); - } - - /** - * Wraps update_manager_update_ready_form(). - * - * @todo Remove update_manager_update_ready_form(). - */ - public function confirmUpdates() { - module_load_include('manager.inc', 'update'); - return \Drupal::formBuilder()->getForm('update_manager_update_ready_form'); - } - -} diff --git a/core/modules/update/src/Form/UpdateManagerInstall.php b/core/modules/update/src/Form/UpdateManagerInstall.php new file mode 100644 index 0000000..b65cc20 --- /dev/null +++ b/core/modules/update/src/Form/UpdateManagerInstall.php @@ -0,0 +1,225 @@ +moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'update_manager_install_form'; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state) { + $this->moduleHandler->loadInclude('update', 'inc', 'update.manager'); + if (!_update_manager_check_backends($form, 'install')) { + return $form; + } + + $form['help_text'] = array( + '#prefix' => '
', + '#markup' => $this->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' => '
', + ); + + $form['project_url'] = array( + '#type' => 'url', + '#title' => $this->t('Install from a URL'), + '#description' => $this->t('For example: %url', array('%url' => 'http://ftp.drupal.org/files/projects/name.tar.gz')), + ); + + $form['information'] = array( + '#prefix' => '', + '#markup' => $this->t('Or'), + '#suffix' => '', + ); + + $form['project_upload'] = array( + '#type' => 'file', + '#title' => $this->t('Upload a module or theme archive to install'), + '#description' => $this->t('For example: %filename from your local computer', array('%filename' => 'name.tar.gz')), + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Install'), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + $uploaded_file = $this->getRequest()->files->get('files[project_upload]', NULL, TRUE); + if (!($form_state['values']['project_url'] XOR !empty($uploaded_file))) { + form_set_error('project_url', $form_state, $this->t('You must either provide a URL or upload an archive file to install.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $field = NULL; + $local_cache = NULL; + if ($form_state['values']['project_url']) { + $field = 'project_url'; + $local_cache = update_manager_file_get($form_state['values']['project_url']); + if (!$local_cache) { + drupal_set_message($this->t('Unable to retrieve Drupal project from %url.', array('%url' => $form_state['values']['project_url'])), 'error'); + return; + } + } + elseif ($_FILES['files']['name']['project_upload']) { + $validators = array('file_validate_extensions' => array(archiver_get_extensions())); + $field = 'project_upload'; + if (!($finfo = file_save_upload($field, $validators, NULL, 0, FILE_EXISTS_REPLACE))) { + // Failed to upload the file. file_save_upload() calls + // drupal_set_message() on failure. + return; + } + $local_cache = $finfo->getFileUri(); + } + + $directory = _update_manager_extract_directory(); + try { + $archive = update_manager_archive_extract($local_cache, $directory); + } + catch (\Exception $e) { + drupal_set_message($e->getMessage(), 'error'); + return; + } + + $files = $archive->listContents(); + if (!$files) { + drupal_set_message($this->t('Provided archive contains no files.'), 'error'); + return; + } + + // Unfortunately, we can only use the directory name to determine the + // project name. Some archivers list the first file as the directory (i.e., + // MODULE/) and others list an actual file (i.e., MODULE/README.TXT). + $project = strtok($files[0], '/\\'); + + $archive_errors = $this->moduleHandler->invokeAll('verify_update_archive', array($project, $local_cache, $directory)); + if (!empty($archive_errors)) { + drupal_set_message(array_shift($archive_errors), 'error'); + // @todo: Fix me in D8: We need a way to set multiple errors on the same + // form element and have all of them appear! + if (!empty($archive_errors)) { + foreach ($archive_errors as $error) { + drupal_set_message($error, 'error'); + } + } + return; + } + + // Make sure the Updater registry is loaded. + drupal_get_updaters(); + + $project_location = $directory . '/' . $project; + try { + $updater = Updater::factory($project_location); + } + catch (\Exception $e) { + drupal_set_message($e->getMessage(), 'error'); + return; + } + + try { + $project_title = Updater::getProjectTitle($project_location); + } + catch (\Exception $e) { + drupal_set_message($e->getMessage(), 'error'); + return; + } + + if (!$project_title) { + drupal_set_message($this->t('Unable to determine %project name.', array('%project' => $project)), 'error'); + } + + if ($updater->isInstalled()) { + drupal_set_message($this->t('%project is already installed.', array('%project' => $project_title)), 'error'); + return; + } + + $project_real_location = drupal_realpath($project_location); + $arguments = array( + 'project' => $project, + 'updater_name' => get_class($updater), + 'local_url' => $project_real_location, + ); + + // If the owner of the directory we extracted is the same as the owner of + // our configuration directory (e.g. sites/default) where we're trying to + // install the code, there's no need to prompt for FTP/SSH credentials. + // Instead, we instantiate a Drupal\Core\FileTransfer\Local and invoke + // update_authorize_run_install() directly. + if (fileowner($project_real_location) == fileowner(conf_path())) { + $this->moduleHandler->loadInclude('update', 'inc', 'update.authorize'); + $filetransfer = new Local(DRUPAL_ROOT); + call_user_func_array('update_authorize_run_install', array_merge(array($filetransfer), $arguments)); + } + + // Otherwise, go through the regular workflow to prompt for FTP/SSH + // credentials and invoke update_authorize_run_install() indirectly with + // whatever FileTransfer object authorize.php creates for us. + else { + system_authorized_init('update_authorize_run_install', drupal_get_path('module', 'update') . '/update.authorize.inc', $arguments, $this->t('Update manager')); + $form_state['redirect'] = system_authorized_get_url(); + } + } + +} diff --git a/core/modules/update/src/Form/UpdateManagerUpdate.php b/core/modules/update/src/Form/UpdateManagerUpdate.php new file mode 100644 index 0000000..d63d3cc --- /dev/null +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php @@ -0,0 +1,331 @@ +moduleHandler = $module_handler; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'update_manager_update_form'; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('module_handler'), + $container->get('state') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state) { + $this->moduleHandler->loadInclude('update', 'inc', 'update.manager'); + + $last_markup = array( + '#theme' => 'update_last_check', + '#last' => $this->state->get('update.last_check') ?: 0, + ); + $form['last_check'] = array( + '#markup' => drupal_render($last_markup), + ); + + if (!_update_manager_check_backends($form, 'update')) { + return $form; + } + + $available = update_get_available(TRUE); + if (empty($available)) { + $form['message'] = array( + '#markup' => $this->t('There was a problem getting update information. Try again later.'), + ); + return $form; + } + + $form['#attached']['library'][] = 'update/drupal.update.admin'; + + // This will be a nested array. The first key is the kind of project, which + // can be either 'enabled', 'disabled', 'manual' (projects which require + // manual updates, such as core). Then, each subarray is an array of + // projects of that type, indexed by project short name, and containing an + // array of data for cells in that project's row in the appropriate table. + $projects = array(); + + // This stores the actual download link we're going to update from for each + // project in the form, regardless of if it's enabled or disabled. + $form['project_downloads'] = array('#tree' => TRUE); + $this->moduleHandler->loadInclude('update', 'inc', 'update.compare'); + $project_data = update_calculate_project_data($available); + foreach ($project_data as $name => $project) { + // Filter out projects which are up to date already. + if ($project['status'] == UPDATE_CURRENT) { + continue; + } + // The project name to display can vary based on the info we have. + if (!empty($project['title'])) { + if (!empty($project['link'])) { + $project_name = l($project['title'], $project['link']); + } + else { + $project_name = String::checkPlain($project['title']); + } + } + elseif (!empty($project['info']['name'])) { + $project_name = String::checkPlain($project['info']['name']); + } + else { + $project_name = String::checkPlain($name); + } + if ($project['project_type'] == 'theme' || $project['project_type'] == 'theme-disabled') { + $project_name .= ' ' . $this->t('(Theme)'); + } + + if (empty($project['recommended'])) { + // If we don't know what to recommend they upgrade to, we should skip + // the project entirely. + continue; + } + + $recommended_release = $project['releases'][$project['recommended']]; + $recommended_version = $recommended_release['version'] . ' ' . l($this->t('(Release notes)'), $recommended_release['release_link'], array('attributes' => array('title' => $this->t('Release notes for @project_title', array('@project_title' => $project['title']))))); + if ($recommended_release['version_major'] != $project['existing_major']) { + $recommended_version .= '' . $this->t('Updates of Drupal core are not supported at this time.') . '
'; + $form['manual_updates'] = array( + '#type' => 'table', + '#header' => $headers, + '#rows' => $projects['manual'], + '#prefix' => $prefix, + '#weight' => 120, + ); + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + if (!empty($form_state['values']['projects'])) { + $enabled = array_filter($form_state['values']['projects']); + } + if (!empty($form_state['values']['disabled_projects'])) { + $disabled = array_filter($form_state['values']['disabled_projects']); + } + if (empty($enabled) && empty($disabled)) { + form_set_error('projects', $form_state, $this->t('You must select at least one project to update.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $this->moduleHandler->loadInclude('update', 'inc', 'update.manager'); + $projects = array(); + foreach (array('projects', 'disabled_projects') as $type) { + if (!empty($form_state['values'][$type])) { + $projects = array_merge($projects, array_keys(array_filter($form_state['values'][$type]))); + } + } + $operations = array(); + foreach ($projects as $project) { + $operations[] = array( + 'update_manager_batch_project_get', + array( + $project, + $form_state['values']['project_downloads'][$project], + ), + ); + } + $batch = array( + 'title' => $this->t('Downloading updates'), + 'init_message' => $this->t('Preparing to download selected updates'), + 'operations' => $operations, + 'finished' => 'update_manager_download_batch_finished', + 'file' => drupal_get_path('module', 'update') . '/update.manager.inc', + ); + batch_set($batch); + } + +} diff --git a/core/modules/update/src/Form/UpdateReady.php b/core/modules/update/src/Form/UpdateReady.php new file mode 100644 index 0000000..57155f0 --- /dev/null +++ b/core/modules/update/src/Form/UpdateReady.php @@ -0,0 +1,148 @@ +moduleHandler = $module_handler; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'update_manager_update_ready_form'; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('module_handler'), + $container->get('state') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state) { + $this->moduleHandler->loadInclude('update', 'inc', 'update.manager'); + if (!_update_manager_check_backends($form, 'update')) { + return $form; + } + + $form['backup'] = array( + '#prefix' => '', + '#markup' => $this->t('Back up your database and site before you continue. Learn how.', array('@backup_url' => url('http://drupal.org/node/22281'))), + '#suffix' => '', + ); + + $form['maintenance_mode'] = array( + '#title' => $this->t('Perform updates with site in maintenance mode (strongly recommended)'), + '#type' => 'checkbox', + '#default_value' => TRUE, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Continue'), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + // Store maintenance_mode setting so we can restore it when done. + $_SESSION['maintenance_mode'] = $this->state->get('system.maintenance_mode'); + if ($form_state['values']['maintenance_mode'] == TRUE) { + $this->state->set('system.maintenance_mode', TRUE); + } + + if (!empty($_SESSION['update_manager_update_projects'])) { + // Make sure the Updater registry is loaded. + drupal_get_updaters(); + + $updates = array(); + $directory = _update_manager_extract_directory(); + + $projects = $_SESSION['update_manager_update_projects']; + unset($_SESSION['update_manager_update_projects']); + + $project_real_location = NULL; + foreach ($projects as $project => $url) { + $project_location = $directory . '/' . $project; + $updater = Updater::factory($project_location); + $project_real_location = drupal_realpath($project_location); + $updates[] = array( + 'project' => $project, + 'updater_name' => get_class($updater), + 'local_url' => $project_real_location, + ); + } + + // If the owner of the last directory we extracted is the same as the + // owner of our configuration directory (e.g. sites/default) where we're + // trying to install the code, there's no need to prompt for FTP/SSH + // credentials. Instead, we instantiate a Drupal\Core\FileTransfer\Local + // and invoke update_authorize_run_update() directly. + if (fileowner($project_real_location) == fileowner(conf_path())) { + $this->moduleHandler->loadInclude('update', 'inc', 'update.authorize'); + $filetransfer = new Local(DRUPAL_ROOT); + update_authorize_run_update($filetransfer, $updates); + } + // Otherwise, go through the regular workflow to prompt for FTP/SSH + // credentials and invoke update_authorize_run_update() indirectly with + // whatever FileTransfer object authorize.php creates for us. + else { + system_authorized_init('update_authorize_run_update', drupal_get_path('module', 'update') . '/update.authorize.inc', array($updates), $this->t('Update manager')); + $form_state['redirect'] = system_authorized_get_url(); + } + } + } + +} diff --git a/core/modules/update/update.manager.inc b/core/modules/update/update.manager.inc index f52d679..66d9647 100644 --- a/core/modules/update/update.manager.inc +++ b/core/modules/update/update.manager.inc @@ -36,315 +36,11 @@ * root. */ -use Drupal\Component\Utility\String; use Drupal\Core\Updater\Updater; use Drupal\Core\FileTransfer\Local; use Symfony\Component\HttpFoundation\RedirectResponse; /** - * Form constructor for the update form of the Update Manager module. - * - * This presents a table with all projects that have available updates with - * checkboxes to select which ones to upgrade. - * - * @param $context - * String representing the context from which we're trying to update. - * Allowed values are 'module', 'theme', and 'report'. - * - * @see update_manager_update_form_validate() - * @see update_manager_update_form_submit() - * @see update_menu() - * - * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. Use - * \Drupal\update\Form\UpdateForm::reportUpdate() or - * \Drupal\update\Form\UpdateForm::moduleUpdate(). - */ -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); - if (empty($available)) { - $form['message'] = array( - '#markup' => t('There was a problem getting update information. Try again later.'), - ); - return $form; - } - - $form['#attached']['library'][] = 'update/drupal.update.admin'; - - // This will be a nested array. The first key is the kind of project, which - // can be either 'enabled', 'disabled', 'manual' (projects which require - // manual updates, such as core). Then, each subarray is an array of - // projects of that type, indexed by project short name, and containing an - // array of data for cells in that project's row in the appropriate table. - $projects = array(); - - // This stores the actual download link we're going to update from for each - // project in the form, regardless of if it's enabled or disabled. - $form['project_downloads'] = array('#tree' => TRUE); - - module_load_include('inc', 'update', 'update.compare'); - $project_data = update_calculate_project_data($available); - foreach ($project_data as $name => $project) { - // Filter out projects which are up to date already. - if ($project['status'] == UPDATE_CURRENT) { - continue; - } - // The project name to display can vary based on the info we have. - if (!empty($project['title'])) { - if (!empty($project['link'])) { - $project_name = l($project['title'], $project['link']); - } - else { - $project_name = String::checkPlain($project['title']); - } - } - elseif (!empty($project['info']['name'])) { - $project_name = String::checkPlain($project['info']['name']); - } - else { - $project_name = String::checkPlain($name); - } - if ($project['project_type'] == 'theme' || $project['project_type'] == 'theme-disabled') { - $project_name .= ' ' . t('(Theme)'); - } - - if (empty($project['recommended'])) { - // If we don't know what to recommend they upgrade to, we should skip - // the project entirely. - continue; - } - - $recommended_release = $project['releases'][$project['recommended']]; - $recommended_version = $recommended_release['version'] . ' ' . l(t('(Release notes)'), $recommended_release['release_link'], array('attributes' => array('title' => t('Release notes for @project_title', array('@project_title' => $project['title']))))); - if ($recommended_release['version_major'] != $project['existing_major']) { - $recommended_version .= '' . t('Updates of Drupal core are not supported at this time.') . '
'; - $form['manual_updates'] = array( - '#type' => 'table', - '#header' => $headers, - '#rows' => $projects['manual'], - '#prefix' => $prefix, - '#weight' => 120, - ); - } - - return $form; -} - -/** - * Returns HTML for the first page in the process of updating projects. - * - * @param $variables - * An associative array containing: - * - form: A render element representing the form. - * - * @ingroup themeable - */ -function theme_update_manager_update_form($variables) { - $form = $variables['form']; - $last = \Drupal::state()->get('update.last_check') ?: 0; - $update_last_check = array( - '#theme' => 'update_last_check', - '#last' => $last, - ); - $output = drupal_render($update_last_check); - $output .= drupal_render_children($form); - return $output; -} - -/** - * Form validation handler for update_manager_update_form(). - * - * Ensures that at least one project is selected. - * - * @see update_manager_update_form_submit() - */ -function update_manager_update_form_validate($form, &$form_state) { - if (!empty($form_state['values']['projects'])) { - $enabled = array_filter($form_state['values']['projects']); - } - if (!empty($form_state['values']['disabled_projects'])) { - $disabled = array_filter($form_state['values']['disabled_projects']); - } - if (empty($enabled) && empty($disabled)) { - form_set_error('projects', $form_state, t('You must select at least one project to update.')); - } -} - -/** - * Form submission handler for update_manager_update_form(). - * - * Sets up a batch that downloads, extracts, and verifies the selected releases. - * - * @see update_manager_update_form_validate() - */ -function update_manager_update_form_submit($form, &$form_state) { - $projects = array(); - foreach (array('projects', 'disabled_projects') as $type) { - if (!empty($form_state['values'][$type])) { - $projects = array_merge($projects, array_keys(array_filter($form_state['values'][$type]))); - } - } - $operations = array(); - foreach ($projects as $project) { - $operations[] = array( - 'update_manager_batch_project_get', - array( - $project, - $form_state['values']['project_downloads'][$project], - ), - ); - } - $batch = array( - 'title' => t('Downloading updates'), - 'init_message' => t('Preparing to download selected updates'), - 'operations' => $operations, - 'finished' => 'update_manager_download_batch_finished', - 'file' => drupal_get_path('module', 'update') . '/update.manager.inc', - ); - batch_set($batch); -} - -/** * Batch callback: Performs actions when the download batch is completed. * * @param $success @@ -374,170 +70,6 @@ function update_manager_download_batch_finished($success, $results) { } /** - * Form constructor for the update ready form. - * - * Build the form when the site is ready to update (after downloading). - * - * This form is an intermediary step in the automated update workflow. It is - * presented to the site administrator after all the required updates have been - * downloaded and verified. The point of this page is to encourage the user to - * backup their site, give them the opportunity to put the site offline, and - * then ask them to confirm that the update should continue. After this step, - * the user is redirected to authorize.php to enter their file transfer - * credentials and attempt to complete the update. - * - * @see update_manager_update_ready_form_submit() - * @see update_menu() - */ -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'))), - '#suffix' => '', - ); - - $form['maintenance_mode'] = array( - '#title' => t('Perform updates with site in maintenance mode (strongly recommended)'), - '#type' => 'checkbox', - '#default_value' => TRUE, - ); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Continue'), - ); - - return $form; -} - -/** - * Form submission handler for update_manager_update_ready_form(). - * - * If the site administrator requested that the site is put offline during the - * update, do so now. Otherwise, pull information about all the required updates - * out of the SESSION, figure out what Drupal\Core\Updater\Updater class is - * needed for each one, generate an array of update operations to perform, and - * hand it all off to system_authorized_init(), then redirect to authorize.php. - * - * @see update_authorize_run_update() - * @see system_authorized_init() - * @see system_authorized_get_url() - */ -function update_manager_update_ready_form_submit($form, &$form_state) { - // Store maintenance_mode setting so we can restore it when done. - $_SESSION['maintenance_mode'] = \Drupal::state()->get('system.maintenance_mode'); - if ($form_state['values']['maintenance_mode'] == TRUE) { - \Drupal::state()->set('system.maintenance_mode', TRUE); - } - - if (!empty($_SESSION['update_manager_update_projects'])) { - // Make sure the Updater registry is loaded. - drupal_get_updaters(); - - $updates = array(); - $directory = _update_manager_extract_directory(); - - $projects = $_SESSION['update_manager_update_projects']; - unset($_SESSION['update_manager_update_projects']); - - foreach ($projects as $project => $url) { - $project_location = $directory . '/' . $project; - $updater = Updater::factory($project_location); - $project_real_location = drupal_realpath($project_location); - $updates[] = array( - 'project' => $project, - 'updater_name' => get_class($updater), - 'local_url' => $project_real_location, - ); - } - - // If the owner of the last directory we extracted is the same as the - // owner of our configuration directory (e.g. sites/default) where we're - // trying to install the code, there's no need to prompt for FTP/SSH - // credentials. Instead, we instantiate a Drupal\Core\FileTransfer\Local and - // invoke update_authorize_run_update() directly. - if (fileowner($project_real_location) == fileowner(conf_path())) { - module_load_include('inc', 'update', 'update.authorize'); - $filetransfer = new Local(DRUPAL_ROOT); - update_authorize_run_update($filetransfer, $updates); - } - // Otherwise, go through the regular workflow to prompt for FTP/SSH - // credentials and invoke update_authorize_run_update() indirectly with - // whatever FileTransfer object authorize.php creates for us. - else { - system_authorized_init('update_authorize_run_update', drupal_get_path('module', 'update') . '/update.authorize.inc', array($updates), t('Update manager')); - $form_state['redirect'] = system_authorized_get_url(); - } - } -} - -/** - * Form constructor for the install form of the Update Manager module. - * - * This presents a place to enter a URL or upload an archive file to use to - * install a new module or theme. - * - * @param $context - * String representing the context from which we're trying to install. - * Allowed values are 'module', 'theme', and 'report'. - * - * @see update_manager_install_form_validate() - * @see update_manager_install_form_submit() - * @see update_menu() - * - * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. Use - * \Drupal\update\Form\UpdateForm::reportInstall() or - * \Drupal\update\Form\UpdateForm::moduleInstall(). - */ -function update_manager_install_form($form, &$form_state, $context) { - if (!_update_manager_check_backends($form, 'install')) { - return $form; - } - - $form['help_text'] = array( - '#prefix' => '', - '#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' => '
', - ); - - $form['project_url'] = array( - '#type' => 'url', - '#title' => t('Install from a URL'), - '#description' => t('For example: %url', array('%url' => 'http://ftp.drupal.org/files/projects/name.tar.gz')), - ); - - $form['information'] = array( - '#prefix' => '', - '#markup' => t('Or'), - '#suffix' => '', - ); - - $form['project_upload'] = array( - '#type' => 'file', - '#title' => t('Upload a module or theme archive to install'), - '#description' => t('For example: %filename from your local computer', array('%filename' => 'name.tar.gz')), - ); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Install'), - ); - - return $form; -} - -/** * Checks for file transfer backends and prepares a form fragment about them. * * @param array $form @@ -605,142 +137,6 @@ function _update_manager_check_backends(&$form, $operation) { } /** - * Form validation handler for update_manager_install_form(). - * - * @see update_manager_install_form_submit() - */ -function update_manager_install_form_validate($form, &$form_state) { - $uploaded_file = \Drupal::request()->files->get('files[project_upload]', NULL, TRUE); - if (!($form_state['values']['project_url'] XOR !empty($uploaded_file))) { - form_set_error('project_url', $form_state, t('You must either provide a URL or upload an archive file to install.')); - } -} - -/** - * Form submission handler for update_manager_install_form(). - * - * Either downloads the file specified in the URL to a temporary cache, or - * uploads the file attached to the form, then attempts to extract the archive - * into a temporary location and verify it. Instantiate the appropriate - * Drupal\Core\Updater\Updater class for this project and make sure it is not - * already installed in the live webroot. If everything is successful, setup an - * operation to run via authorize.php which will copy the extracted files from - * the temporary location into the live site. - * - * @see update_manager_install_form_validate() - * @see update_authorize_run_install() - * @see system_authorized_init() - * @see system_authorized_get_url() - */ -function update_manager_install_form_submit($form, &$form_state) { - if ($form_state['values']['project_url']) { - $field = 'project_url'; - $local_cache = update_manager_file_get($form_state['values']['project_url']); - if (!$local_cache) { - drupal_set_message(t('Unable to retrieve Drupal project from %url.', array('%url' => $form_state['values']['project_url'])), 'error'); - return; - } - } - elseif ($_FILES['files']['name']['project_upload']) { - $validators = array('file_validate_extensions' => array(archiver_get_extensions())); - $field = 'project_upload'; - if (!($finfo = file_save_upload($field, $validators, NULL, 0, FILE_EXISTS_REPLACE))) { - // Failed to upload the file. file_save_upload() calls - // drupal_set_message() on failure. - return; - } - $local_cache = $finfo->getFileUri(); - } - - $directory = _update_manager_extract_directory(); - try { - $archive = update_manager_archive_extract($local_cache, $directory); - } - catch (Exception $e) { - drupal_set_message($e->getMessage(), 'error'); - return; - } - - $files = $archive->listContents(); - if (!$files) { - drupal_set_message(t('Provided archive contains no files.'), 'error'); - return; - } - - // Unfortunately, we can only use the directory name to determine the project - // name. Some archivers list the first file as the directory (i.e., MODULE/) - // and others list an actual file (i.e., MODULE/README.TXT). - $project = strtok($files[0], '/\\'); - - $archive_errors = update_manager_archive_verify($project, $local_cache, $directory); - if (!empty($archive_errors)) { - drupal_set_message(array_shift($archive_errors), 'error'); - // @todo: Fix me in D8: We need a way to set multiple errors on the same - // form element and have all of them appear! - if (!empty($archive_errors)) { - foreach ($archive_errors as $error) { - drupal_set_message($error, 'error'); - } - } - return; - } - - // Make sure the Updater registry is loaded. - drupal_get_updaters(); - - $project_location = $directory . '/' . $project; - try { - $updater = Updater::factory($project_location); - } - catch (Exception $e) { - drupal_set_message($e->getMessage(), 'error'); - return; - } - - try { - $project_title = Updater::getProjectTitle($project_location); - } - catch (Exception $e) { - drupal_set_message($e->getMessage(), 'error'); - return; - } - - if (!$project_title) { - drupal_set_message(t('Unable to determine %project name.', array('%project' => $project)), 'error'); - } - - if ($updater->isInstalled()) { - drupal_set_message(t('%project is already installed.', array('%project' => $project_title)), 'error'); - return; - } - - $project_real_location = drupal_realpath($project_location); - $arguments = array( - 'project' => $project, - 'updater_name' => get_class($updater), - 'local_url' => $project_real_location, - ); - - // If the owner of the directory we extracted is the same as the - // owner of our configuration directory (e.g. sites/default) where we're - // trying to install the code, there's no need to prompt for FTP/SSH - // credentials. Instead, we instantiate a Drupal\Core\FileTransfer\Local and - // invoke update_authorize_run_install() directly. - if (fileowner($project_real_location) == fileowner(conf_path())) { - module_load_include('inc', 'update', 'update.authorize'); - $filetransfer = new Local(DRUPAL_ROOT); - call_user_func_array('update_authorize_run_install', array_merge(array($filetransfer), $arguments)); - } - // Otherwise, go through the regular workflow to prompt for FTP/SSH - // credentials and invoke update_authorize_run_install() indirectly with - // whatever FileTransfer object authorize.php creates for us. - else { - system_authorized_init('update_authorize_run_install', drupal_get_path('module', 'update') . '/update.authorize.inc', $arguments, t('Update manager')); - $form_state['redirect'] = system_authorized_get_url(); - } -} - -/** * Unpacks a downloaded archive file. * * @param string $file @@ -903,8 +299,6 @@ function update_manager_batch_project_get($project, $url, &$context) { * @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() { diff --git a/core/modules/update/update.module b/core/modules/update/update.module index 1dbfcf9..06f2dd6 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -172,10 +172,6 @@ function update_manager_access() { */ function update_theme() { return array( - 'update_manager_update_form' => array( - 'render element' => 'form', - 'file' => 'update.manager.inc', - ), 'update_last_check' => array( 'variables' => array('last' => 0), 'template' => 'update-last-check', diff --git a/core/modules/update/update.routing.yml b/core/modules/update/update.routing.yml index 2ed1b68..d3cf59a 100644 --- a/core/modules/update/update.routing.yml +++ b/core/modules/update/update.routing.yml @@ -25,7 +25,7 @@ update.manual_status: update.report_install: path: '/admin/reports/updates/install' defaults: - _content: '\Drupal\update\Form\UpdateForm::reportInstall' + _form: '\Drupal\update\Form\UpdateManagerInstall' _title: 'Install' options: _access_mode: 'ALL' @@ -36,7 +36,7 @@ update.report_install: update.report_update: path: '/admin/reports/updates/update' defaults: - _content: '\Drupal\update\Form\UpdateForm::reportUpdate' + _form: '\Drupal\update\Form\UpdateManagerUpdate' _title: 'Update' options: _access_mode: 'ALL' @@ -47,7 +47,7 @@ update.report_update: update.module_install: path: '/admin/modules/install' defaults: - _content: '\Drupal\update\Form\UpdateForm::moduleInstall' + _form: '\Drupal\update\Form\UpdateManagerInstall' _title: 'Install new module' options: _access_mode: 'ALL' @@ -58,7 +58,7 @@ update.module_install: update.module_update: path: '/admin/modules/update' defaults: - _content: '\Drupal\update\Form\UpdateForm::moduleUpdate' + _form: '\Drupal\update\Form\UpdateManagerUpdate' _title: 'Update' options: _access_mode: 'ALL' @@ -69,7 +69,7 @@ update.module_update: update.theme_install: path: '/admin/theme/install' defaults: - _content: '\Drupal\update\Form\UpdateForm::themeInstall' + _form: '\Drupal\update\Form\UpdateManagerInstall' _title: 'Install new theme' options: _access_mode: 'ALL' @@ -80,7 +80,7 @@ update.theme_install: update.theme_update: path: '/admin/theme/update' defaults: - _content: '\Drupal\update\Form\UpdateForm::themeUpdate' + _form: '\Drupal\update\Form\UpdateManagerUpdate' _title: 'Update' options: _access_mode: 'ALL' @@ -91,7 +91,7 @@ update.theme_update: update.confirmation_page: path: '/admin/update/ready' defaults: - _content: '\Drupal\update\Form\UpdateForm::confirmUpdates' + _form: '\Drupal\update\Form\UpdateReady' _title: 'Ready to update' options: _access_mode: 'ALL' diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php index c27e3c9..5997735 100644 --- a/core/modules/user/src/Controller/UserController.php +++ b/core/modules/user/src/Controller/UserController.php @@ -11,6 +11,10 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\user\UserInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Drupal\Core\Datetime\Date; +use Drupal\user\UserStorageInterface; /** * Controller routines for user routes. @@ -18,6 +22,112 @@ class UserController extends ControllerBase { /** + * The date formatting service. + * + * @var \Drupal\Core\Datetime\Date + */ + protected $date; + + /** + * The user storage. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; + + /** + * Constructs a UserController object. + * + * @param \Drupal\Core\Datetime\Date $date + * The date formatting service. + * @param \Drupal\user\UserStorageInterface $user_storage + * The user storage. + */ + public function __construct(Date $date, UserStorageInterface $user_storage) { + $this->date = $date; + $this->userStorage = $user_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('date'), + $container->get('entity.manager')->getStorage('user') + ); + } + + /** + * Returns the user password reset page. + * + * @param int $uid + * UID of user requesting reset. + * @param int $timestamp + * The current timestamp. + * @param string $hash + * Login link hash. + * + * @return array|\Symfony\Component\HttpFoundation\RedirectResponse + * The form structure or a redirect response. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * If the login link is for a blocked user or invalid user ID. + */ + public function resetPass($uid, $timestamp, $hash) { + $account = $this->currentUser(); + $config = $this->config('user.settings'); + // When processing the one-time login link, we have to make sure that a user + // isn't already logged in. + if ($account->isAuthenticated()) { + // The current user is already logged in. + if ($account->id() == $uid) { + drupal_set_message($this->t('You are logged in as %user. Change your password.', array('%user' => $account->getUsername(), '!user_edit' => $this->url('user.edit', array('user' => $account->id()))))); + } + // A different user is already logged in on the computer. + else { + $reset_link_user = $this->userStorage->load($uid); + if ($reset_link_user) { + drupal_set_message($this->t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please logout and try using the link again.', + array('%other_user' => $account->getUsername(), '%resetting_user' => $reset_link_user->getUsername(), '!logout' => $this->url('user.logout')))); + } + else { + // Invalid one-time link specifies an unknown user. + drupal_set_message($this->t('The one-time login link you clicked is invalid.')); + } + } + return $this->redirect('This is a one-time login for %user_name and will expire on %expiration_date.
Click on this button to log in to the site and change your password.
', array('%user_name' => $user->getUsername(), '%expiration_date' => $expiration_date))); + } + else { + // No expiration for first time login. + $form['message'] = array('#markup' => $this->t('This is a one-time login for %user_name.
Click on this button to log in to the site and change your password.
', array('%user_name' => $user->getUsername()))); + } + + $form['#title'] = 'Reset Password'; + $form['user'] = array( + '#type' => 'value', + '#value' => $user, + ); + $form['timestamp'] = array( + '#type' => 'value', + '#value' => $timestamp, + ); + $form['help'] = array('#markup' => '' . $this->t('This login can be used only once.') . '
'); + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Log in'), + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + /** @var $user \Drupal\user\UserInterface */ + $user = $form_state['values']['user']; + user_login_finalize($user); + $this->logger->notice('User %name used one-time login link at time %timestamp.', array('%name' => $user->getUsername(), '%timestamp' => $form_state['values']['timestamp'])); + drupal_set_message($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.')); + // Let the user's password be changed without the current password check. + $token = Crypt::randomBytesBase64(55); + $_SESSION['pass_reset_' . $user->id()] = $token; + $form_state['redirect_route']['route_name'] = 'user.edit'; + $form_state['redirect_route']['route_parameters'] = array('user' => $user->id()); + $form_state['redirect_route']['options'] = array( + 'query' => array('pass-reset-token' => $token), + 'absolute' => TRUE, + ); + } + +} + diff --git a/core/modules/user/src/Tests/UserPasswordResetTest.php b/core/modules/user/src/Tests/UserPasswordResetTest.php index 41adecc..1045e2c 100644 --- a/core/modules/user/src/Tests/UserPasswordResetTest.php +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php @@ -106,6 +106,13 @@ function testUserPasswordReset() { $_uid = $this->account->id(); $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account->getPassword(), $bogus_timestamp, $this->account->getLastLoginTime())); $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.'); + + // Create a user, block the account, and verify that a login link is denied. + $timestamp = REQUEST_TIME - 1; + $blocked_account = $this->drupalCreateUser()->block(); + $blocked_account->save(); + $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account->getPassword(), $timestamp, $blocked_account->getLastLoginTime())); + $this->assertResponse(403); } /** diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc index 4b6a0ae..4a09d7b 100644 --- a/core/modules/user/user.pages.inc +++ b/core/modules/user/user.pages.inc @@ -6,97 +6,8 @@ */ use Drupal\Core\Render\Element; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Drupal\Component\Utility\Crypt; - -/** - * Menu callback; process one time login link and redirects to the user page on success. - * - * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. - * Use \Drupal\user\Form\UserForm::resetPass(). - */ -function user_pass_reset($form, &$form_state, $uid, $timestamp, $hashed_pass, $action = NULL) { - global $user; - - // When processing the one-time login link, we have to make sure that a user - // isn't already logged in. - if ($user->isAuthenticated()) { - // The existing user is already logged in. - if ($user->id() == $uid) { - drupal_set_message(t('You are logged in as %user. Change your password.', array('%user' => $user->getUsername(), '!user_edit' => url("user/" . $user->id() . "/edit")))); - } - // A different user is already logged in on the computer. - else { - $reset_link_account = user_load($uid); - if (!empty($reset_link_account)) { - drupal_set_message(t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. Please logout and try using the link again.', - array('%other_user' => $user->getUsername(), '%resetting_user' => $reset_link_account->getUsername(), '!logout' => url('user/logout')))); - } else { - // Invalid one-time link specifies an unknown user. - drupal_set_message(t('The one-time login link you clicked is invalid.')); - } - } - return new RedirectResponse(url('This is a one-time login for %user_name.
Click on this button to log in to the site and change your password.
', array('%user_name' => $account->getUsername()))); - } - else { - $form['message'] = array('#markup' => t('This is a one-time login for %user_name and will expire on %expiration_date.
Click on this button to log in to the site and change your password.
', array('%user_name' => $account->getUsername(), '%expiration_date' => format_date($timestamp + $timeout)))); - } - $form['help'] = array('#markup' => '' . t('This login can be used only once.') . '
'); - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in')); - $form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login"); - return $form; - } - } - else { - drupal_set_message(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.')); - return new RedirectResponse(url('user/password', array('absolute' => TRUE))); - } - } - else { - // Deny access, no more clues. - // Everything will be in the watchdog's URL for the administrator to check. - throw new AccessDeniedHttpException(); - } - } -} /** * Prepares variables for user templates. diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 0a7b842..087ffec 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -182,11 +182,10 @@ user.cancel_confirm: _entity_access: 'user.delete' user.reset: - path: '/user/reset/{uid}/{timestamp}/{hash}/{operation}' + path: '/user/reset/{uid}/{timestamp}/{hash}' defaults: - _content: '\Drupal\user\Form\UserForm::resetPass' + _content: '\Drupal\user\Controller\UserController::resetPass' _title: 'Reset password' - operation: NULL requirements: _access: 'TRUE' options: