From 3afec501d4f08945cfaa08c99b1d26c4e0eac7d9 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Tue, 2 Sep 2014 10:28:33 +0100 Subject: [PATCH] Issue #2250119 by ParisLiakos, Devin Carlson: Run updates in a full environment. --- .htaccess | 1 - core/includes/theme.inc | 41 ++ core/includes/theme.maintenance.inc | 40 -- core/includes/update.inc | 132 +--- .../UpdateServiceProvider.php | 66 -- .../Update/Form/UpdateScriptSelectionForm.php | 134 ---- .../system/src/Access/DbUpdateAccessCheck.php | 36 ++ .../src/Controller/DbUpdateController.php | 612 ++++++++++++++++++ .../Tests/Update/InvalidUpdateHookTest.php | 4 +- .../src/Tests/Update/UpdateScriptTest.php | 52 +- .../src/Tests/Update/UpdatesWith7xTest.php | 5 +- .../system/src/Theme/DbUpdateNegotiator.php | 57 ++ core/modules/system/system.routing.yml | 10 +- core/modules/system/system.services.yml | 9 + core/update.php | 427 ------------ 15 files changed, 787 insertions(+), 839 deletions(-) delete mode 100644 core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php delete mode 100644 core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php create mode 100644 core/modules/system/src/Access/DbUpdateAccessCheck.php create mode 100644 core/modules/system/src/Controller/DbUpdateController.php create mode 100644 core/modules/system/src/Theme/DbUpdateNegotiator.php delete mode 100644 core/update.php diff --git a/.htaccess b/.htaccess index 5ca73051447c..c32b18211985 100644 --- a/.htaccess +++ b/.htaccess @@ -113,7 +113,6 @@ DirectoryIndex index.php index.html index.htm # RewriteBase / # Redirect common PHP files to their new locations. - RewriteCond %{REQUEST_URI} ^(.*)?/(update.php) [OR] RewriteCond %{REQUEST_URI} ^(.*)?/(install.php) [OR] RewriteCond %{REQUEST_URI} ^(.*)?/(rebuild.php) RewriteCond %{REQUEST_URI} !core diff --git a/core/includes/theme.inc b/core/includes/theme.inc index d811f4d27021..278677497595 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1574,6 +1574,47 @@ function template_preprocess_container(&$variables) { $variables['attributes'] = $element['#attributes']; } + +/** + * Returns HTML for a list of maintenance tasks to perform. + * + * @param $variables + * An associative array containing: + * - items: An associative array of maintenance tasks. + * It's the caller's responsibility to ensure this array's items contain no + * dangerous HTML such as SCRIPT tags. + * - active: The key for the currently active maintenance task. + * + * @ingroup themeable + */ +function theme_task_list($variables) { + $items = $variables['items']; + $active = $variables['active']; + + $done = isset($items[$active]) || $active == NULL; + $output = '<h2 class="visually-hidden">Installation tasks</h2>'; + $output .= '<ol class="task-list">'; + + foreach ($items as $k => $item) { + if ($active == $k) { + $class = 'active'; + $status = '(' . t('active') . ')'; + $done = FALSE; + } + else { + $class = $done ? 'done' : ''; + $status = $done ? '(' . t('done') . ')' : ''; + } + $output .= '<li'; + $output .= ($class ? ' class="' . $class . '"' : '') . '>'; + $output .= $item; + $output .= ($status ? '<span class="visually-hidden"> ' . $status . '</span>' : ''); + $output .= '</li>'; + } + $output .= '</ol>'; + return $output; +} + /** * @} End of "addtogroup themeable". */ diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 2ceec4a5d198..0a9fb5aaba31 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -103,46 +103,6 @@ function _drupal_maintenance_theme() { Drupal::service('theme.registry'); } -/** - * Returns HTML for a list of maintenance tasks to perform. - * - * @param $variables - * An associative array containing: - * - items: An associative array of maintenance tasks. - * It's the caller's responsibility to ensure this array's items contain no - * dangerous HTML such as SCRIPT tags. - * - active: The key for the currently active maintenance task. - * - * @ingroup themeable - */ -function theme_task_list($variables) { - $items = $variables['items']; - $active = $variables['active']; - - $done = isset($items[$active]) || $active == NULL; - $output = '<h2 class="visually-hidden">Installation tasks</h2>'; - $output .= '<ol class="task-list">'; - - foreach ($items as $k => $item) { - if ($active == $k) { - $class = 'active'; - $status = '(' . t('active') . ')'; - $done = FALSE; - } - else { - $class = $done ? 'done' : ''; - $status = $done ? '(' . t('done') . ')' : ''; - } - $output .= '<li'; - $output .= ($class ? ' class="' . $class . '"' : '') . '>'; - $output .= $item; - $output .= ($status ? '<span class="visually-hidden"> ' . $status . '</span>' : ''); - $output .= '</li>'; - } - $output .= '</ol>'; - return $output; -} - /** * Returns HTML for a results report of an operation run by authorize.php. * diff --git a/core/includes/update.inc b/core/includes/update.inc index 945ffe750e30..f9a88db9f55a 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -131,33 +131,13 @@ function update_system_schema_requirements() { /** * Checks update requirements and reports errors and (optionally) warnings. - * - * @param $skip_warnings - * (optional) If set to TRUE, requirement warnings will be ignored, and a - * report will only be issued if there are requirement errors. Defaults to - * FALSE. */ -function update_check_requirements($skip_warnings = FALSE) { +function update_check_requirements() { // Check requirements of all loaded modules. $requirements = \Drupal::moduleHandler()->invokeAll('requirements', array('update')); $requirements += update_system_schema_requirements(); $requirements += update_settings_file_requirements(); - $severity = drupal_requirements_severity($requirements); - - // If there are errors, always display them. If there are only warnings, skip - // them if the caller has indicated they should be skipped. - if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && !$skip_warnings)) { - $regions['sidebar_first'] = update_task_list('requirements'); - $status_report = array( - '#theme' => 'status_report', - '#requirements' => $requirements, - ); - $status_report['#suffix'] = 'Check the messages and <a href="' . check_url(drupal_requirements_url($severity)) . '">try again</a>.'; - - drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); - print DefaultHtmlPageRenderer::renderPage($status_report, 'Requirements problem', 'maintenance', $regions); - exit(); - } + return $requirements; } /** @@ -277,114 +257,6 @@ function update_do_one($module, $number, $dependency_map, &$context) { $context['message'] = 'Updating ' . String::checkPlain($module) . ' module'; } -/** - * Starts the database update batch process. - * - * @param $start - * An array whose keys contain the names of modules to be updated during the - * current batch process, and whose values contain the number of the first - * requested update for that module. The actual updates that are run (and the - * order they are run in) will depend on the results of passing this data - * through the update dependency system. - * @param $redirect - * Path to redirect to when the batch has finished processing. - * @param $url - * URL of the batch processing page (should only be used for separate - * scripts like update.php). - * @param $batch - * Optional parameters to pass into the batch API. - * @param $redirect_callback - * (optional) Specify a function to be called to redirect to the progressive - * processing page. - * - * @see update_resolve_dependencies() - */ -function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $redirect_callback = NULL) { - // During the update, bring the site offline so that schema changes do not - // affect visiting users. - $maintenance_mode = \Drupal::config('system.maintenance')->get('enabled'); - if (isset($maintenance_mode)) { - $_SESSION['maintenance_mode'] = $maintenance_mode; - } - if (empty($_SESSION['maintenance_mode'])) { - if (db_table_exists('state')) { - \Drupal::state()->set('system.maintenance_mode', TRUE); - } - } - - // Resolve any update dependencies to determine the actual updates that will - // be run and the order they will be run in. - $updates = update_resolve_dependencies($start); - - // Store the dependencies for each update function in an array which the - // batch API can pass in to the batch operation each time it is called. (We - // do not store the entire update dependency array here because it is - // potentially very large.) - $dependency_map = array(); - foreach ($updates as $function => $update) { - $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); - } - - $operations = array(); - foreach ($updates as $update) { - if ($update['allowed']) { - // Set the installed version of each module so updates will start at the - // correct place. (The updates are already sorted, so we can simply base - // this on the first one we come across in the above foreach loop.) - if (isset($start[$update['module']])) { - drupal_set_installed_schema_version($update['module'], $update['number'] - 1); - unset($start[$update['module']]); - } - // Add this update function to the batch. - $function = $update['module'] . '_update_' . $update['number']; - $operations[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function])); - } - } - $batch['operations'] = $operations; - $batch += array( - 'title' => 'Updating', - 'init_message' => 'Starting updates', - 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.', - 'finished' => 'update_finished', - 'file' => 'core/includes/update.inc', - ); - batch_set($batch); - return batch_process($redirect, $url, $redirect_callback); -} - -/** - * Finishes the update process and stores the results for eventual display. - * - * After the updates run, all caches are flushed. The update results are - * stored into the session (for example, to be displayed on the update results - * page in update.php). Additionally, if the site was off-line, now that the - * update process is completed, the site is set back online. - * - * @param $success - * Indicate that the batch API tasks were all completed successfully. - * @param $results - * An array of all the results that were updated in update_do_one(). - * @param $operations - * A list of all the operations that had not been completed by the batch API. - * - * @see update_batch() - */ -function update_finished($success, $results, $operations) { - // Clear the caches in case the data has been updated. - update_flush_all_caches(); - - $_SESSION['update_results'] = $results; - $_SESSION['update_success'] = $success; - $_SESSION['updates_remaining'] = $operations; - - // Now that the update is done, we can put the site back online if it was - // previously in maintenance mode. - if (isset($_SESSION['maintenance_mode'])) { - \Drupal::state()->set('system.maintenance_mode', FALSE); - unset($_SESSION['maintenance_mode']); - } -} - /** * Returns a list of all the pending database updates. * diff --git a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php b/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php deleted file mode 100644 index d8a40aa4a913..000000000000 --- a/core/lib/Drupal/Core/DependencyInjection/UpdateServiceProvider.php +++ /dev/null @@ -1,66 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\Core\DependencyInjection\UpdateServiceProvider. - */ - -namespace Drupal\Core\DependencyInjection; - -use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpFoundation\Request; - -/** - * ServiceProvider class for update.php service overrides. - * - * This class is manually added by update.php via $conf['container_service_providers'] - * and required to prevent various services from trying to retrieve data from - * storages that do not exist yet. - */ -class UpdateServiceProvider implements ServiceProviderInterface, ServiceModifierInterface { - - /** - * {@inheritdoc} - */ - public function register(ContainerBuilder $container) { - if (!empty($GLOBALS['conf']['update_service_provider_overrides'])) { - // Disable the Lock service. - $container - ->register('lock', 'Drupal\Core\Lock\NullLockBackend'); - - // Prevent config from being accessed via a cache wrapper by removing - // any existing definition and setting an alias to the actual storage. - $container->removeDefinition('config.storage'); - $container->setAlias('config.storage', 'config.storage.active'); - - $container - ->register('cache_factory', 'Drupal\Core\Cache\MemoryBackendFactory'); - $container - ->register('router.builder', 'Drupal\Core\Routing\RouteBuilderStatic'); - - $container->register('theme_handler', 'Drupal\Core\Extension\ThemeHandler') - ->addArgument(new Reference('config.factory')) - ->addArgument(new Reference('module_handler')) - ->addArgument(new Reference('state')) - ->addArgument(new Reference('info_parser')) - ->addArgument(new Reference('logger.channel.default')) - ->addArgument(new Reference('asset.css.collection_optimizer')); - } - } - - /** - * {@inheritdoc} - */ - public function alter(ContainerBuilder $container) { - // Ensure that URLs generated for the home and admin pages don't have - // 'update.php' in them. - $request = Request::createFromGlobals(); - $definition = $container->getDefinition('url_generator'); - $definition->addMethodCall('setBasePath', array(str_replace('/core', '', $request->getBasePath()) . '/')); - // We need to set the script path to an empty string since the value - // determined by \Drupal\Core\Routing\UrlGenerator::setRequest() is invalid - // once '/core' has been removed from the base path. - $definition->addMethodCall('setScriptPath', array('')); - } - -} diff --git a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php b/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php deleted file mode 100644 index 574ec6c46db7..000000000000 --- a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\Core\Update\Form\UpdateScriptSelectionForm. - */ - -namespace Drupal\Core\Update\Form; - -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; - -/** - * Provides the list of available database module updates. - */ -class UpdateScriptSelectionForm extends FormBase { - - /** - * {@inheritdoc} - */ - public function getFormID() { - return 'update_script_selection_form'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $count = 0; - $incompatible_count = 0; - $form['start'] = array( - '#tree' => TRUE, - '#type' => 'details', - ); - - // Ensure system.module's updates appear first. - $form['start']['system'] = array(); - - $updates = update_get_update_list(); - $starting_updates = array(); - $incompatible_updates_exist = FALSE; - foreach ($updates as $module => $update) { - if (!isset($update['start'])) { - $form['start'][$module] = array( - '#type' => 'item', - '#title' => $module . ' module', - '#markup' => $update['warning'], - '#prefix' => '<div class="messages messages--warning">', - '#suffix' => '</div>', - ); - $incompatible_updates_exist = TRUE; - continue; - } - if (!empty($update['pending'])) { - $starting_updates[$module] = $update['start']; - $form['start'][$module] = array( - '#type' => 'hidden', - '#value' => $update['start'], - ); - $form['start'][$module . '_updates'] = array( - '#theme' => 'item_list', - '#items' => $update['pending'], - '#title' => $module . ' module', - ); - } - if (isset($update['pending'])) { - $count = $count + count($update['pending']); - } - } - - // Find and label any incompatible updates. - foreach (update_resolve_dependencies($starting_updates) as $data) { - if (!$data['allowed']) { - $incompatible_updates_exist = TRUE; - $incompatible_count++; - $module_update_key = $data['module'] . '_updates'; - if (isset($form['start'][$module_update_key]['#items'][$data['number']])) { - $text = $data['missing_dependencies'] ? 'This update will been skipped due to the following missing dependencies: <em>' . implode(', ', $data['missing_dependencies']) . '</em>' : "This update will be skipped due to an error in the module's code."; - $form['start'][$module_update_key]['#items'][$data['number']] .= '<div class="warning">' . $text . '</div>'; - } - // Move the module containing this update to the top of the list. - $form['start'] = array($module_update_key => $form['start'][$module_update_key]) + $form['start']; - } - } - - // Warn the user if any updates were incompatible. - if ($incompatible_updates_exist) { - drupal_set_message('Some of the pending updates cannot be applied because their dependencies were not met.', 'warning'); - } - - if (empty($count)) { - drupal_set_message(t('No pending updates.')); - unset($form); - $form['links'] = array( - '#theme' => 'links', - '#links' => update_helpful_links(), - ); - - // No updates to run, so caches won't get flushed later. Clear them now. - update_flush_all_caches(); - } - else { - $form['help'] = array( - '#markup' => '<p>The version of Drupal you are updating from has been automatically detected.</p>', - '#weight' => -5, - ); - if ($incompatible_count) { - $form['start']['#title'] = format_plural( - $count, - '1 pending update (@number_applied to be applied, @number_incompatible skipped)', - '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', - array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) - ); - } - else { - $form['start']['#title'] = format_plural($count, '1 pending update', '@count pending updates'); - } - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => 'Apply pending updates', - '#button_type' => 'primary', - ); - } - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - } - -} diff --git a/core/modules/system/src/Access/DbUpdateAccessCheck.php b/core/modules/system/src/Access/DbUpdateAccessCheck.php new file mode 100644 index 000000000000..0d37db196370 --- /dev/null +++ b/core/modules/system/src/Access/DbUpdateAccessCheck.php @@ -0,0 +1,36 @@ +<?php + +/** + * @file + * Contains Drupal\system\Access\DbUpdateAccessCheck. + */ + +namespace Drupal\system\Access; + +use Drupal\Core\Access\AccessInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Site\Settings; + +/** + * Access check for database update routes. + */ +class DbUpdateAccessCheck implements AccessInterface { + + /** + * Checks access for update routes. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The currently logged in account. + * + * @return string + * A \Drupal\Core\Access\AccessInterface constant value. + */ + public function access(AccountInterface $account) { + // Allow the global variable in settings.php to override the access check. + if (Settings::get('update_free_access')) { + return static::ALLOW; + } + + return $account->hasPermission('administer software updates') ? static::ALLOW : static::KILL; + } +} diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php new file mode 100644 index 000000000000..62752aebe015 --- /dev/null +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -0,0 +1,612 @@ +<?php + +/** + * @file + * Contains \Drupal\system\Controller\DbUpdateController. + */ + +namespace Drupal\system\Controller; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; +use Drupal\Core\Page\DefaultHtmlPageRenderer; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Site\Settings; +use Drupal\Core\State\StateInterface; +use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; + +/** + * Controller routines for database update routes. + */ +class DbUpdateController extends ControllerBase { + + /** + * The keyvalue expirable factory. + * + * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface + */ + protected $keyValueExpirableFactory; + + /** + * A cache backend interface. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $account; + + /** + * Constructs a new UpdateController. + * + * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory + * The keyvalue expirable factory. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * A cache backend interface. + * @param \Drupal\Core\State\StateInterface $state + * The state service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user. + */ + public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account) { + $this->keyValueExpirableFactory = $key_value_expirable_factory; + $this->cache = $cache; + $this->state = $state; + $this->moduleHandler = $module_handler; + $this->account = $account; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('keyvalue.expirable'), + $container->get('cache.default'), + $container->get('state'), + $container->get('module_handler'), + $container->get('current_user') + ); + } + + /** + * Returns a database update page. + * + * @param string $op + * The update operation to perform. Can be any of the below: + * - info + * - selection + * - run + * - results + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * + * @return \Symfony\Component\HttpFoundation\Response + * A response object object. + */ + public function handle($op, Request $request) { + require_once DRUPAL_ROOT . '/core/includes/install.inc'; + require_once DRUPAL_ROOT . '/core/includes/update.inc'; + + drupal_load_updates(); + update_fix_compatibility(); + + if ($request->query->get('continue')) { + $_SESSION['update_ignore_warnings'] = TRUE; + } + + $regions = array(); + $requirements = update_check_requirements(); + $severity = drupal_requirements_severity($requirements); + if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($_SESSION['update_ignore_warnings']))) { + $regions['sidebar_first'] = $this->updateTasksList('requirements'); + $output = $this->requirements($severity, $requirements); + } + else { + switch ($op) { + case 'selection': + $regions['sidebar_first'] = $this->updateTasksList('selection'); + $output = $this->selection(); + break; + + case 'run': + $regions['sidebar_first'] = $this->updateTasksList('run'); + $output = $this->triggerBatch($request); + break; + + case 'info': + $regions['sidebar_first'] = $this->updateTasksList('info'); + $output = $this->info(); + break; + + case 'results': + $regions['sidebar_first'] = $this->updateTasksList('results'); + $output = $this->results(); + break; + + // Regular batch ops : defer to batch processing API. + default: + require_once DRUPAL_ROOT . '/core/includes/batch.inc'; + $regions['sidebar_first'] = $this->updateTasksList('run'); + $output = _batch_page($request); + break; + } + } + + if ($output instanceof Response) { + return $output; + } + $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update'); + + return new Response(DefaultHtmlPageRenderer::renderPage($output, $title, 'maintenance', $regions)); + } + + /** + * Returns the info database update page. + * + * @return array + * A render array. + */ + protected function info() { + // Change query-strings on css/js files to enforce reload for all users. + _drupal_flush_css_js(); + // Flush the cache of all data for the update status module. + $this->keyValueExpirableFactory->get('update')->deleteAll(); + $this->keyValueExpirableFactory->get('update_available_release')->deleteAll(); + + $build['info_header'] = array( + '#markup' => '<p>' . $this->t('Use this utility to update your database whenever a new release of Drupal or a module is installed.') . '</p><p>' . $this->t('For more detailed information, see the <a href="http://drupal.org/upgrade">upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.') . '</p>', + ); + + $info[] = $this->t("<strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism."); + $info[] = $this->t('Put your site into <a href="@url">maintenance mode</a>.', array( + '@url' => $this->url('system.site_maintenance_mode'), + )); + $info[] = $this->t('<strong>Back up your database</strong>. This process will change your database values and in case of emergency you may need to revert to a backup.'); + $info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.'); + $build['info'] = array( + '#theme' => 'item_list', + '#list_type' => 'ol', + '#items' => $info, + ); + $build['info_footer'] = array( + '#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>', + ); + + $url = new Url('system.db_update', array('op' => 'selection')); + $build['link'] = array( + '#type' => 'link', + '#title' => $this->t('Continue'), + '#attributes' => array('class' => array('button', 'button--primary')), + ) + $url->toRenderArray(); + return $build; + } + + /** + * Renders a list of available database updates. + * + * @return array + * A render array. + */ + protected function selection() { + // Make sure there is no stale theme registry. + $this->cache->deleteAll(); + + $count = 0; + $incompatible_count = 0; + $build['start'] = array( + '#tree' => TRUE, + '#type' => 'details', + ); + + // Ensure system.module's updates appear first. + $build['start']['system'] = array(); + + $updates = update_get_update_list(); + $starting_updates = array(); + $incompatible_updates_exist = FALSE; + foreach ($updates as $module => $update) { + if (!isset($update['start'])) { + $build['start'][$module] = array( + '#type' => 'item', + '#title' => $module . ' module', + '#markup' => $update['warning'], + '#prefix' => '<div class="messages messages--warning">', + '#suffix' => '</div>', + ); + $incompatible_updates_exist = TRUE; + continue; + } + if (!empty($update['pending'])) { + $starting_updates[$module] = $update['start']; + $build['start'][$module] = array( + '#type' => 'hidden', + '#value' => $update['start'], + ); + $build['start'][$module . '_updates'] = array( + '#theme' => 'item_list', + '#items' => $update['pending'], + '#title' => $module . ' module', + ); + } + if (isset($update['pending'])) { + $count = $count + count($update['pending']); + } + } + + // Find and label any incompatible updates. + foreach (update_resolve_dependencies($starting_updates) as $data) { + if (!$data['allowed']) { + $incompatible_updates_exist = TRUE; + $incompatible_count++; + $module_update_key = $data['module'] . '_updates'; + if (isset($build['start'][$module_update_key]['#items'][$data['number']])) { + if ($data['missing_dependencies']) { + $text = $this->t('This update will been skipped due to the following missing dependencies:') . '<em>' . implode(', ', $data['missing_dependencies']) . '</em>'; + } + else { + $text = $this->t("This update will be skipped due to an error in the module's code."); + } + $build['start'][$module_update_key]['#items'][$data['number']] .= '<div class="warning">' . $text . '</div>'; + } + // Move the module containing this update to the top of the list. + $build['start'] = array($module_update_key => $build['start'][$module_update_key]) + $build['start']; + } + } + + // Warn the user if any updates were incompatible. + if ($incompatible_updates_exist) { + drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning'); + } + + if (empty($count)) { + drupal_set_message($this->t('No pending updates.')); + unset($build); + $build['links'] = array( + '#theme' => 'links', + '#links' => $this->helpfulLinks(), + ); + + // No updates to run, so caches won't get flushed later. Clear them now. + drupal_flush_all_caches(); + } + else { + $build['help'] = array( + '#markup' => '<p>' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '</p>', + '#weight' => -5, + ); + if ($incompatible_count) { + $build['start']['#title'] = $this->formatPlural( + $count, + '1 pending update (@number_applied to be applied, @number_incompatible skipped)', + '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', + array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) + ); + } + else { + $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates'); + } + $url = new Url('system.db_update', array('op' => 'run')); + $build['link'] = array( + '#type' => 'link', + '#title' => $this->t('Apply pending updates'), + '#attributes' => array('class' => array('button', 'button--primary')), + '#weight' => 5, + ) + $url->toRenderArray(); + } + + return $build; + } + + /** + * Displays results of the update script with any accompanying errors. + * + * @return array + * A render array. + */ + protected function results() { + // Report end result. + $dblog_exists = $this->moduleHandler->moduleExists('dblog'); + if ($dblog_exists && $this->account->hasPermission('access site reports')) { + $log_message = $this->t('All errors have been <a href="@url">logged</a>.', array( + '@url' => $this->url('dblog.overview'), + )); + } + else { + $log_message = $this->t('All errors have been logged.'); + } + + if (!empty($_SESSION['update_success'])) { + $message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => $this->url('<front>'))) . ' ' . $log_message . '</p>'; + } + else { + $last = reset($_SESSION['updates_remaining']); + list($module, $version) = array_pop($last); + $message = '<p class="error">' . $this->t('The update process was aborted prematurely while running <strong>update #@version in @module.module</strong>.', array( + '@version' => $version, + '@module' => $module, + )) . ' ' . $log_message; + if ($dblog_exists) { + $message .= ' ' . $this->t('You may need to check the <code>watchdog</code> database table manually.'); + } + $message .= '</p>'; + } + + if (Settings::get('update_free_access')) { + $message .= '<p>' . $this->t("<strong>Reminder: don't forget to set the <code>\$settings['update_free_access']</code> value in your <code>settings.php</code> file back to <code>FALSE</code>.</strong>") . '</p>'; + } + + $build['message'] = array( + '#markup' => $message, + ); + $build['links'] = array( + '#theme' => 'links', + '#links' => $this->helpfulLinks(), + ); + + // Output a list of info messages. + if (!empty($_SESSION['update_results'])) { + $all_messages = array(); + foreach ($_SESSION['update_results'] as $module => $updates) { + if ($module != '#abort') { + $module_has_message = FALSE; + $info_messages = array(); + foreach ($updates as $number => $queries) { + $messages = array(); + foreach ($queries as $query) { + // If there is no message for this update, don't show anything. + if (empty($query['query'])) { + continue; + } + + if ($query['success']) { + $messages[] = array( + '#wrapper_attributes' => array('class' => array('success')), + '#markup' => $query['query'], + ); + } + else { + $messages[] = array( + '#wrapper_attributes' => array('class' => array('failure')), + '#markup' => '<strong>' . $this->t('Failed:') . '</strong> ' . $query['query'], + ); + } + } + + if ($messages) { + $module_has_message = TRUE; + $info_messages[] = array( + '#theme' => 'item_list', + '#items' => $messages, + '#title' => $this->t('Update #@count', array('@count' => $number)), + ); + } + } + + // If there were any messages then prefix them with the module name + // and add it to the global message list. + if ($module_has_message) { + $all_messages[] = array( + '#type' => 'container', + '#prefix' => '<h3>' . $this->t('@module module', array('@module' => $module)) . '</h3>', + '#children' => $info_messages, + ); + } + } + } + if ($all_messages) { + $build['query_messsages'] = array( + '#type' => 'container', + '#children' => $all_messages, + '#attributes' => array('class' => array('update-results')), + '#prefix' => '<h2>' . $this->t('The following updates returned messages:') . '</h2>', + ); + } + } + unset($_SESSION['update_results']); + unset($_SESSION['update_success']); + unset($_SESSION['update_ignore_warnings']); + + return $build; + } + + /** + * Renders a list of requirement errors or warnings. + * + * @return array + * A render array. + */ + public function requirements($severity, array $requirements) { + $options = $severity == REQUIREMENT_WARNING ? array('continue' => 1) : array(); + $try_again_url = $this->url('system.db_update', $options); + + $build['status_report'] = array( + '#theme' => 'status_report', + '#requirements' => $requirements, + '#suffix' => $this->t('Check the messages and <a href="@url">try again</a>.', array('@url' => $try_again_url)) + ); + + $build['#title'] = $this->t('Requirements problem'); + return $build; + } + + /** + * Provides the update task list render array. + * + * @param string $active + * The active task. + * Can be one of 'requirements', 'info', 'selection', 'run', 'results'. + * + * @return array + * A render array. + */ + protected function updateTasksList($active = NULL) { + // Default list of tasks. + $tasks = array( + 'requirements' => $this->t('Verify requirements'), + 'info' => $this->t('Overview'), + 'selection' => $this->t('Review updates'), + 'run' => $this->t('Run updates'), + 'results' => $this->t('Review log'), + ); + + $task_list = array( + '#theme' => 'task_list', + '#items' => $tasks, + '#active' => $active, + ); + return $task_list; + } + + /** + * Starts the database update batch process. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + */ + protected function triggerBatch(Request $request) { + // During the update, bring the site offline so that schema changes do not + // affect visiting users. + $maintenance_mode = $this->config('system.maintenance')->get('enabled'); + if (isset($maintenance_mode)) { + $_SESSION['maintenance_mode'] = $maintenance_mode; + } + if (empty($_SESSION['maintenance_mode'])) { + $this->state->set('system.maintenance_mode', TRUE); + } + + $start = $this->getModuleUpdates(); + // Resolve any update dependencies to determine the actual updates that will + // be run and the order they will be run in. + $updates = update_resolve_dependencies($start); + + // Store the dependencies for each update function in an array which the + // batch API can pass in to the batch operation each time it is called. (We + // do not store the entire update dependency array here because it is + // potentially very large.) + $dependency_map = array(); + foreach ($updates as $function => $update) { + $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); + } + + $operations = array(); + foreach ($updates as $update) { + if ($update['allowed']) { + // Set the installed version of each module so updates will start at the + // correct place. (The updates are already sorted, so we can simply base + // this on the first one we come across in the above foreach loop.) + if (isset($start[$update['module']])) { + drupal_set_installed_schema_version($update['module'], $update['number'] - 1); + unset($start[$update['module']]); + } + // Add this update function to the batch. + $function = $update['module'] . '_update_' . $update['number']; + $operations[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function])); + } + } + $batch['operations'] = $operations; + $batch += array( + 'title' => $this->t('Updating'), + 'init_message' => $this->t('Starting updates'), + 'error_message' => $this->t('An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.'), + 'finished' => array('\Drupal\system\Controller\DbUpdateController', 'batchFinished'), + ); + batch_set($batch); + + return batch_process('update.php/results', 'update.php/batch'); + } + + /** + * Finishes the update process and stores the results for eventual display. + * + * After the updates run, all caches are flushed. The update results are + * stored into the session (for example, to be displayed on the update results + * page in update.php). Additionally, if the site was off-line, now that the + * update process is completed, the site is set back online. + * + * @param $success + * Indicate that the batch API tasks were all completed successfully. + * @param array $results + * An array of all the results that were updated in update_do_one(). + * @param array $operations + * A list of all the operations that had not been completed by the batch API. + */ + public static function batchFinished($success, $results, $operations) { + // No updates to run, so caches won't get flushed later. Clear them now. + drupal_flush_all_caches(); + + $_SESSION['update_results'] = $results; + $_SESSION['update_success'] = $success; + $_SESSION['updates_remaining'] = $operations; + + // Now that the update is done, we can put the site back online if it was + // previously in maintenance mode. + if (isset($_SESSION['maintenance_mode'])) { + \Drupal::state()->set('system.maintenance_mode', FALSE); + unset($_SESSION['maintenance_mode']); + } + } + + /** + * Provides links to the homepage and administration pages. + * + * @return array + * An array of links. + */ + protected function helpfulLinks() { + $links['front'] = array( + 'title' => $this->t('Front page'), + 'href' => '<front>', + ); + if ($this->account->hasPermission('access administration pages')) { + $links['admin-pages'] = array( + 'title' => $this->t('Administration pages'), + 'href' => 'admin', + ); + } + return $links; + } + + /** + * Retrieves module updates. + * + * @return array + * The module updates that can be performed. + */ + protected function getModuleUpdates() { + $return = array(); + $updates = update_get_update_list(); + foreach ($updates as $module => $update) { + $return[$module] = $update['start']; + } + + return $return; + } + +} diff --git a/core/modules/system/src/Tests/Update/InvalidUpdateHookTest.php b/core/modules/system/src/Tests/Update/InvalidUpdateHookTest.php index d07300adead0..b5ea8fc14bd5 100644 --- a/core/modules/system/src/Tests/Update/InvalidUpdateHookTest.php +++ b/core/modules/system/src/Tests/Update/InvalidUpdateHookTest.php @@ -43,7 +43,7 @@ protected function setUp() { parent::setUp(); require_once DRUPAL_ROOT . '/core/includes/update.inc'; - $this->update_url = $GLOBALS['base_url'] . '/core/update.php'; + $this->update_url = $GLOBALS['base_url'] . '/update.php'; $this->update_user = $this->drupalCreateUser(array('administer software updates')); } @@ -51,7 +51,7 @@ function testInvalidUpdateHook() { // Confirm that a module with hook_update_8000() cannot be updated. $this->drupalLogin($this->update_user); $this->drupalGet($this->update_url); - $this->drupalPostForm($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->clickLink(t('Continue')); $this->assertText(t('Some of the pending updates cannot be applied because their dependencies were not met.')); } diff --git a/core/modules/system/src/Tests/Update/UpdateScriptTest.php b/core/modules/system/src/Tests/Update/UpdateScriptTest.php index 54e72dca08d2..893682ec0e0b 100644 --- a/core/modules/system/src/Tests/Update/UpdateScriptTest.php +++ b/core/modules/system/src/Tests/Update/UpdateScriptTest.php @@ -30,30 +30,8 @@ class UpdateScriptTest extends WebTestBase { protected function setUp() { parent::setUp(); - $this->update_url = $GLOBALS['base_url'] . '/core/update.php'; - $this->update_user = $this->drupalCreateUser(array('administer software updates')); - } - - /** - * Tests that updates from schema versions prior to 8000 are prevented. - */ - function testInvalidMigration() { - // Mock a D7 system table so that the schema value of the system module - // can be retrieved. - db_create_table('system', $this->getSystemSchema()); - // Assert that the table exists. - $this->assertTrue(db_table_exists('system'), 'The table exists.'); - // Insert a value for the system module. - db_insert('system') - ->fields(array( - 'name' => 'system', - 'schema_version' => 7000, - )) - ->execute(); - $system_schema = db_query('SELECT schema_version FROM {system} WHERE name = :system', array(':system' => 'system'))->fetchField(); - $this->drupalGet($this->update_url, array('external' => TRUE)); - $text = 'Your system schema version is ' . $system_schema . '. Updating directly from a schema version prior to 8000 is not supported. You must <a href="https://drupal.org/node/2179269">migrate your site to Drupal 8</a> first.'; - $this->assertRaw($text, 'Updates from schema versions prior to 8000 are prevented.'); + $this->update_url = $GLOBALS['base_url'] . '/update.php'; + $this->update_user = $this->drupalCreateUser(array('administer software updates', 'access site in maintenance mode')); } /** @@ -92,7 +70,7 @@ function testRequirements() { // If there are no requirements warnings or errors, we expect to be able to // go through the update process uninterrupted. $this->drupalGet($this->update_url, array('external' => TRUE)); - $this->drupalPostForm(NULL, array(), t('Continue')); + $this->clickLink(t('Continue')); $this->assertText(t('No pending updates.'), 'End of update process was reached.'); // Confirm that all caches were cleared. $this->assertText(t('hook_cache_flush() invoked for update_script_test.module.'), 'Caches were cleared when there were no requirements warnings or errors.'); @@ -109,8 +87,8 @@ function testRequirements() { $this->assertText('This is a requirements warning provided by the update_script_test module.'); $this->clickLink('try again'); $this->assertNoText('This is a requirements warning provided by the update_script_test module.'); - $this->drupalPostForm(NULL, array(), t('Continue')); - $this->drupalPostForm(NULL, array(), 'Apply pending updates'); + $this->clickLink(t('Continue')); + $this->clickLink(t('Apply pending updates')); $this->assertText(t('The update_script_test_update_8001() update was executed successfully.'), 'End of update process was reached.'); // Confirm that all caches were cleared. $this->assertText(t('hook_cache_flush() invoked for update_script_test.module.'), 'Caches were cleared after resolving a requirements warning and applying updates.'); @@ -120,7 +98,7 @@ function testRequirements() { $this->assertText('This is a requirements warning provided by the update_script_test module.'); $this->clickLink('try again'); $this->assertNoText('This is a requirements warning provided by the update_script_test module.'); - $this->drupalPostForm(NULL, array(), t('Continue')); + $this->clickLink(t('Continue')); $this->assertText(t('No pending updates.'), 'End of update process was reached.'); // Confirm that all caches were cleared. $this->assertText(t('hook_cache_flush() invoked for update_script_test.module.'), 'Caches were cleared after applying updates and re-running the script.'); @@ -155,7 +133,8 @@ function testThemeSystem() { function testNoUpdateFunctionality() { // Click through update.php with 'administer software updates' permission. $this->drupalLogin($this->update_user); - $this->drupalPostForm($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->clickLink(t('Continue')); $this->assertText(t('No pending updates.')); $this->assertNoLink('Administration pages'); $this->assertNoLinkByHref('update.php', 0); @@ -165,7 +144,8 @@ function testNoUpdateFunctionality() { // Click through update.php with 'access administration pages' permission. $admin_user = $this->drupalCreateUser(array('administer software updates', 'access administration pages')); $this->drupalLogin($admin_user); - $this->drupalPostForm($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->clickLink(t('Continue')); $this->assertText(t('No pending updates.')); $this->assertLink('Administration pages'); $this->assertNoLinkByHref('update.php', 1); @@ -187,8 +167,9 @@ function testSuccessfulUpdateFunctionality() { // Click through update.php with 'administer software updates' permission. $this->drupalLogin($this->update_user); - $this->drupalPostForm($this->update_url, array(), t('Continue'), array('external' => TRUE)); - $this->drupalPostForm(NULL, array(), t('Apply pending updates')); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->clickLink(t('Continue')); + $this->clickLink(t('Apply pending updates')); // Verify that updates were completed successfully. $this->assertText('Updates were attempted.'); @@ -219,10 +200,11 @@ function testSuccessfulUpdateFunctionality() { // Click through update.php with 'access administration pages' and // 'access site reports' permissions. - $admin_user = $this->drupalCreateUser(array('administer software updates', 'access administration pages', 'access site reports')); + $admin_user = $this->drupalCreateUser(array('administer software updates', 'access administration pages', 'access site reports', 'access site in maintenance mode')); $this->drupalLogin($admin_user); - $this->drupalPostForm($this->update_url, array(), t('Continue'), array('external' => TRUE)); - $this->drupalPostForm(NULL, array(), t('Apply pending updates')); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->clickLink(t('Continue')); + $this->clickLink(t('Apply pending updates')); $this->assertText('Updates were attempted.'); $this->assertLink('logged'); $this->assertLink('Administration pages'); diff --git a/core/modules/system/src/Tests/Update/UpdatesWith7xTest.php b/core/modules/system/src/Tests/Update/UpdatesWith7xTest.php index 7b04dc6892ab..b6c215daedc6 100644 --- a/core/modules/system/src/Tests/Update/UpdatesWith7xTest.php +++ b/core/modules/system/src/Tests/Update/UpdatesWith7xTest.php @@ -37,7 +37,7 @@ class UpdatesWith7xTest extends WebTestBase { protected function setUp() { parent::setUp(); require_once DRUPAL_ROOT . '/core/includes/update.inc'; - $this->update_url = $GLOBALS['base_url'] . '/core/update.php'; + $this->update_url = $GLOBALS['base_url'] . '/update.php'; $this->update_user = $this->drupalCreateUser(array('administer software updates')); } @@ -52,7 +52,8 @@ function testWith7x() { // Click through update.php with 'administer software updates' permission. $this->drupalLogin($this->update_user); - $this->drupalPostForm($this->update_url, array(), t('Continue'), array('external' => TRUE)); + $this->drupalGet($this->update_url, array('external' => TRUE)); + $this->clickLink(t('Continue')); $this->assertText(t('Some of the pending updates cannot be applied because their dependencies were not met.')); } } diff --git a/core/modules/system/src/Theme/DbUpdateNegotiator.php b/core/modules/system/src/Theme/DbUpdateNegotiator.php new file mode 100644 index 000000000000..260923ca453f --- /dev/null +++ b/core/modules/system/src/Theme/DbUpdateNegotiator.php @@ -0,0 +1,57 @@ +<?php + +/** + * @file + * Contains \Drupal\system\Theme\DbUpdateNegotiator. + */ + +namespace Drupal\system\Theme; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Site\Settings; +use Drupal\Core\Theme\ThemeNegotiatorInterface; + +/** + * Sets the active theme for the database update pages. + */ +class DbUpdateNegotiator implements ThemeNegotiatorInterface { + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Constructs a DbUpdateNegotiator. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + */ + public function __construct(ConfigFactoryInterface $config_factory) { + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function applies(RouteMatchInterface $route_match) { + return $route_match->getRouteName() == 'system.db_update'; + } + + /** + * {@inheritdoc} + */ + public function determineActiveTheme(RouteMatchInterface $route_match) { + $custom_theme = Settings::get('maintenance_theme', 'seven'); + if (!$custom_theme) { + $config = $this->configFactory->get('system.theme'); + $custom_theme = $config->get('default'); + } + + return $custom_theme; + } + +} diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index ba2a46d26b35..80348633ed2b 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -407,8 +407,14 @@ system.batch_page.json: options: _admin_route: TRUE -system.update: - path: '/core/update.php' +system.db_update: + path: '/update.php/{op}' + defaults: + _title: 'Drupal database update' + _controller: '\Drupal\system\Controller\DbUpdateController::handle' + op: 'info' + requirements: + _access_system_update: 'TRUE' system.admin_content: path: '/admin/content' diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index a5b94ced97d4..453eea4f9393 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -3,6 +3,10 @@ services: class: Drupal\system\Access\CronAccessCheck tags: - { name: access_check, applies_to: _access_system_cron } + access_check.db_update: + class: Drupal\system\Access\DbUpdateAccessCheck + tags: + - { name: access_check, applies_to: _access_system_update } system.manager: class: Drupal\system\SystemManager arguments: ['@module_handler', '@database', '@entity.manager', '@request_stack', '@menu.link_tree', '@menu.active_trail'] @@ -26,6 +30,11 @@ services: arguments: ['@batch.storage', '@request_stack'] tags: - { name: theme_negotiator, priority: 1000 } + theme.negotiator.system.db_update: + class: Drupal\system\Theme\DbUpdateNegotiator + arguments: ['@config.factory'] + tags: + - { name: theme_negotiator, priority: 100 } system.config_subscriber: class: Drupal\system\SystemConfigSubscriber tags: diff --git a/core/update.php b/core/update.php deleted file mode 100644 index 4e8157c46b8e..000000000000 --- a/core/update.php +++ /dev/null @@ -1,427 +0,0 @@ -<?php - -/** - * @file - * Administrative page for handling updates from one Drupal version to another. - * - * Point your browser to "http://www.example.com/core/update.php" and follow the - * instructions. - * - * If you are not logged in using either the site maintenance account or an - * account with the "Administer software updates" permission, you will need to - * modify the access check statement inside your settings.php file. After - * finishing the upgrade, be sure to open settings.php again, and change it - * back to its original state! - */ - -use Drupal\Core\DrupalKernel; -use Drupal\Core\Page\DefaultHtmlPageRenderer; -use Drupal\Core\Site\Settings; -use Drupal\Core\Update\Form\UpdateScriptSelectionForm; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\DependencyInjection\Reference; - -// Change the directory to the Drupal root. -chdir('..'); - -$autoloader = require_once __DIR__ . '/vendor/autoload.php'; - -// Exit early if an incompatible PHP version would cause fatal errors. -// The minimum version is specified explicitly, as DRUPAL_MINIMUM_PHP is not -// yet available. It is defined in bootstrap.inc, but it is not possible to -// load that file yet as it would cause a fatal error on older versions of PHP. -if (version_compare(PHP_VERSION, '5.4.2') < 0) { - print 'Your PHP installation is too old. Drupal requires at least PHP 5.4.2. See the <a href="http://drupal.org/requirements">system requirements</a> page for more information.'; - exit; -} - -/** - * Global flag indicating that update.php is being run. - * - * When this flag is set, various operations do not take place, such as css/js - * preprocessing and translation. - * - * This constant is defined using define() instead of const so that PHP - * versions older than 5.3 can display the proper PHP requirements instead of - * causing a fatal error. - */ -define('MAINTENANCE_MODE', 'update'); - -/** - * Renders a form with a list of available database updates. - */ -function update_selection_page() { - // Make sure there is no stale theme registry. - \Drupal::cache()->deleteAll(); - - $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateScriptSelectionForm'); - $build['#title'] = 'Drupal database update'; - - return $build; -} - -/** - * Provides links to the homepage and administration pages. - */ -function update_helpful_links() { - $links['front'] = array( - 'title' => t('Front page'), - 'href' => '<front>', - ); - if (\Drupal::currentUser()->hasPermission('access administration pages')) { - $links['admin-pages'] = array( - 'title' => t('Administration pages'), - 'href' => 'admin', - ); - } - return $links; -} - -/** - * Remove update overrides and flush all caches. - * - * This will need to be run once all (if any) updates are run. Do not call this - * while updates are running. - */ -function update_flush_all_caches() { - $GLOBALS['conf']['update_service_provider_overrides'] = FALSE; - \Drupal::service('kernel')->updateModules(\Drupal::moduleHandler()->getModuleList()); - - // No updates to run, so caches won't get flushed later. Clear them now. - drupal_flush_all_caches(); -} - -/** - * Displays results of the update script with any accompanying errors. - */ -function update_results_page() { - // Report end result. - if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) { - $log_message = ' All errors have been <a href="' . base_path() . '?q=admin/reports/dblog">logged</a>.'; - } - else { - $log_message = ' All errors have been logged.'; - } - - if ($_SESSION['update_success']) { - $output = '<p>Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="' . base_path() . '">site</a>. Otherwise, you may need to update your database manually.' . $log_message . '</p>'; - } - else { - $last = reset($_SESSION['updates_remaining']); - list($module, $version) = array_pop($last); - $output = '<p class="error">The update process was aborted prematurely while running <strong>update #' . $version . ' in ' . $module . '.module</strong>.' . $log_message; - if (\Drupal::moduleHandler()->moduleExists('dblog')) { - $output .= ' You may need to check the <code>watchdog</code> database table manually.'; - } - $output .= '</p>'; - } - - if (Settings::get('update_free_access')) { - $output .= "<p><strong>Reminder: don't forget to set the <code>\$settings['update_free_access']</code> value in your <code>settings.php</code> file back to <code>FALSE</code>.</strong></p>"; - } - - $links = array( - '#theme' => 'links', - '#links' => update_helpful_links(), - ); - $output .= drupal_render($links); - - // Output a list of queries executed. - if (!empty($_SESSION['update_results'])) { - $all_messages = ''; - foreach ($_SESSION['update_results'] as $module => $updates) { - if ($module != '#abort') { - $module_has_message = FALSE; - $query_messages = ''; - foreach ($updates as $number => $queries) { - $messages = array(); - foreach ($queries as $query) { - // If there is no message for this update, don't show anything. - if (empty($query['query'])) { - continue; - } - - if ($query['success']) { - $messages[] = '<li class="success">' . $query['query'] . '</li>'; - } - else { - $messages[] = '<li class="failure"><strong>Failed:</strong> ' . $query['query'] . '</li>'; - } - } - - if ($messages) { - $module_has_message = TRUE; - $query_messages .= '<h4>Update #' . $number . "</h4>\n"; - $query_messages .= '<ul>' . implode("\n", $messages) . "</ul>\n"; - } - } - - // If there were any messages in the queries then prefix them with the - // module name and add it to the global message list. - if ($module_has_message) { - $all_messages .= '<h3>' . $module . " module</h3>\n" . $query_messages; - } - } - } - if ($all_messages) { - $output .= '<div class="update-results"><h2>The following updates returned messages</h2>'; - $output .= $all_messages; - $output .= '</div>'; - } - } - unset($_SESSION['update_results']); - unset($_SESSION['update_success']); - - $build = array( - '#title' => 'Drupal database update', - '#markup' => $output, - ); - return $build; -} - -/** - * Provides an overview of the Drupal database update. - * - * This page provides cautionary suggestions that should happen before - * proceeding with the update to ensure data integrity. - * - * @return - * Rendered HTML form. - */ -function update_info_page() { - // Change query-strings on css/js files to enforce reload for all users. - _drupal_flush_css_js(); - // Flush the cache of all data for the update status module. - $keyvalue = \Drupal::service('keyvalue.expirable'); - $keyvalue->get('update')->deleteAll(); - $keyvalue->get('update_available_release')->deleteAll(); - - $token = \Drupal::csrfToken()->get('update'); - $output = '<p>Use this utility to update your database whenever a new release of Drupal or a module is installed.</p><p>For more detailed information, see the <a href="http://drupal.org/upgrade">upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.</p>'; - $output .= "<ol>\n"; - $output .= "<li><strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.</li>\n"; - $output .= '<li>Put your site into <a href="' . base_path() . '?q=admin/config/development/maintenance">maintenance mode</a>.</li>' . "\n"; - $output .= "<li><strong>Back up your database</strong>. This process will change your database values and in case of emergency you may need to revert to a backup.</li>\n"; - $output .= "<li>Install your new files in the appropriate location, as described in the handbook.</li>\n"; - $output .= "</ol>\n"; - $output .= "<p>When you have performed the steps above, you may proceed.</p>\n"; - $form_action = check_url(drupal_current_script_url(array('op' => 'selection', 'token' => $token))); - $output .= '<form method="post" action="' . $form_action . '"><div class="form-actions form-wrapper" id="edit-actions"><input type="submit" value="Continue" class="button button--primary form-submit" /></div></form>'; - $output .= "\n"; - - $build = array( - '#title' => 'Drupal database update', - '#markup' => $output, - ); - return $build; -} - -/** - * Renders a 403 access denied page for update.php. - * - * @return - * Rendered HTML warning with 403 status. - */ -function update_access_denied_page() { - drupal_add_http_header('Status', '403 Forbidden'); - header(\Drupal::request()->server->get('SERVER_PROTOCOL') . ' 403 Forbidden'); - \Drupal::logger('access denied')->warning('update.php'); - $output = '<p>Access denied. You are not authorized to access this page. Log in using either an account with the <em>administer software updates</em> permission or the site maintenance account (the account you created during installation). If you cannot log in, you will have to edit <code>settings.php</code> to bypass this access check. To do this:</p> -<ol> - <li>With a text editor find the settings.php file on your system. From the main Drupal directory that you installed all the files into, go to <code>sites/your_site_name</code> if such directory exists, or else to <code>sites/default</code> which applies otherwise.</li> - <li>There is a line inside your settings.php file that says <code>$settings[\'update_free_access\'] = FALSE;</code>. Change it to <code>$settings[\'update_free_access\'] = TRUE;</code>.</li> - <li>As soon as the update.php script is done, you must change the settings.php file back to its original form with <code>$settings[\'update_free_access\'] = FALSE;</code>.</li> - <li>To avoid having this problem in the future, remember to log in to your website using either an account with the <em>administer software updates</em> permission or the site maintenance account (the account you created during installation) before you backup your database at the beginning of the update process.</li> -</ol>'; - - $build = array( - '#title' => 'Access denied', - '#markup' => $output, - ); - return $build; -} - -/** - * Determines if the current user is allowed to run update.php. - * - * @return - * TRUE if the current user should be granted access, or FALSE otherwise. - */ -function update_access_allowed() { - return Settings::get('update_free_access') || \Drupal::currentUser()->hasPermission('administer software updates'); -} - -/** - * Adds the update task list to the current page. - */ -function update_task_list($active = NULL) { - // Default list of tasks. - $tasks = array( - 'requirements' => 'Verify requirements', - 'info' => 'Overview', - 'select' => 'Review updates', - 'run' => 'Run updates', - 'finished' => 'Review log', - ); - - $task_list = array( - '#theme' => 'task_list', - '#items' => $tasks, - '#active' => $active, - ); - return $task_list; -} - -// Some unavoidable errors happen because the database is not yet up-to-date. -// Our custom error handler is not yet installed, so we just suppress them. -ini_set('display_errors', FALSE); - -// We prepare a minimal bootstrap for the update requirements check to avoid -// reaching the PHP memory limit. -require_once __DIR__ . '/includes/update.inc'; -require_once __DIR__ . '/includes/install.inc'; - -$request = Request::createFromGlobals(); -$kernel = DrupalKernel::createFromRequest($request, $autoloader, 'update', FALSE); - -// Enable UpdateServiceProvider service overrides. -// @see update_flush_all_caches() -$GLOBALS['conf']['container_service_providers']['UpdateServiceProvider'] = 'Drupal\Core\DependencyInjection\UpdateServiceProvider'; -$GLOBALS['conf']['update_service_provider_overrides'] = TRUE; -$kernel->boot(); - -// Updating from a site schema version prior to 8000 should block the update -// process. Ensure that the site is not attempting to update a database -// created in a previous version of Drupal. -if (db_table_exists('system')) { - $system_schema = db_query('SELECT schema_version FROM {system} WHERE name = :system', array(':system' => 'system'))->fetchField(); - if ($system_schema < \Drupal::CORE_MINIMUM_SCHEMA_VERSION) { - print 'Your system schema version is ' . $system_schema . '. Updating directly from a schema version prior to 8000 is not supported. You must <a href="https://drupal.org/node/2179269">migrate your site to Drupal 8</a> first.'; - exit; - } -} - -$kernel->prepareLegacyRequest($request); - -// Determine if the current user has access to run update.php. -\Drupal::service('session_manager')->startLazy(); - -// Ensure that URLs generated for the home and admin pages don't have 'update.php' -// in them. -$generator = \Drupal::urlGenerator(); -$generator->setBasePath(str_replace('/core', '', $request->getBasePath()) . '/'); -$generator->setScriptPath(''); - -// There can be conflicting 'op' parameters because both update and batch use -// this parameter name. We need the 'op' coming from a POST request to trump -// that coming from a GET request. -$op = $request->request->get('op'); -if (is_null($op)) { - $op = $request->query->get('op'); -} - -// Only allow the requirements check to proceed if the current user has access -// to run updates (since it may expose sensitive information about the site's -// configuration). -if (is_null($op) && update_access_allowed()) { - require_once __DIR__ . '/includes/install.inc'; - require_once DRUPAL_ROOT . '/core/modules/system/system.install'; - - // Set up theme system for the maintenance page. - drupal_maintenance_theme(); - - // Check the update requirements for Drupal. Only report on errors at this - // stage, since the real requirements check happens further down. - // The request will exit() if any requirement violations are reported in the - // following function invocation. - update_check_requirements(TRUE); - - // Redirect to the update information page if all requirements were met. - install_goto('core/update.php?op=info'); -} - -drupal_maintenance_theme(); - -// Turn error reporting back on. From now on, only fatal errors (which are -// not passed through the error handler) will cause a message to be printed. -ini_set('display_errors', TRUE); - -$regions = array(); - -// Only proceed with updates if the user is allowed to run them. -if (update_access_allowed()) { - - include_once __DIR__ . '/includes/install.inc'; - include_once __DIR__ . '/includes/batch.inc'; - drupal_load_updates(); - - update_fix_compatibility(); - - // Check the update requirements for all modules. If there are warnings, but - // no errors, skip reporting them if the user has provided a URL parameter - // acknowledging the warnings and indicating a desire to continue anyway. See - // drupal_requirements_url(). - $continue = $request->query->get('continue'); - $skip_warnings = !empty($continue); - update_check_requirements($skip_warnings); - - switch ($op) { - // update.php ops. - - case 'selection': - $token = $request->query->get('token'); - if (isset($token) && \Drupal::csrfToken()->validate($token, 'update')) { - $regions['sidebar_first'] = update_task_list('select'); - $output = update_selection_page(); - break; - } - - case 'Apply pending updates': - $token = $request->query->get('token'); - if (isset($token) && \Drupal::csrfToken()->validate($token, 'update')) { - $regions['sidebar_first'] = update_task_list('run'); - // Generate absolute URLs for the batch processing (using $base_root), - // since the batch API will pass them to url() which does not handle - // update.php correctly by default. - $batch_url = $base_root . drupal_current_script_url(); - $redirect_url = $base_root . drupal_current_script_url(array('op' => 'results')); - $output = update_batch($request->request->get('start'), $redirect_url, $batch_url); - break; - } - - case 'info': - $regions['sidebar_first'] = update_task_list('info'); - $output = update_info_page(); - break; - - case 'results': - $regions['sidebar_first'] = update_task_list(); - $output = update_results_page(); - break; - - // Regular batch ops : defer to batch processing API. - default: - $regions['sidebar_first'] = update_task_list('run'); - $output = _batch_page($request); - break; - } -} -else { - $output = update_access_denied_page(); -} -if (isset($output) && $output) { - // Explicitly start a session so that the update.php token will be accepted. - \Drupal::service('session_manager')->start(); - // We defer the display of messages until all updates are done. - $progress_page = ($batch = batch_get()) && isset($batch['running']); - if ($output instanceof Response) { - $output->send(); - } - else { - drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); - print DefaultHtmlPageRenderer::renderPage($output, $output['#title'], 'maintenance', $regions + array( - '#show_messages' => !$progress_page, - )); - } -} -- GitLab