3 namespace Drupal\system\Controller;
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Controller\ControllerBase;
7 use Drupal\Core\Extension\ModuleHandlerInterface;
8 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
9 use Drupal\Core\Render\BareHtmlPageRendererInterface;
10 use Drupal\Core\Session\AccountInterface;
11 use Drupal\Core\Site\Settings;
12 use Drupal\Core\State\StateInterface;
13 use Drupal\Core\Update\UpdateRegistry;
15 use Symfony\Component\DependencyInjection\ContainerInterface;
16 use Symfony\Component\HttpFoundation\Response;
17 use Symfony\Component\HttpFoundation\Request;
20 * Controller routines for database update routes.
22 class DbUpdateController extends ControllerBase {
25 * The keyvalue expirable factory.
27 * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
29 protected $keyValueExpirableFactory;
32 * A cache backend interface.
34 * @var \Drupal\Core\Cache\CacheBackendInterface
41 * @var \Drupal\Core\State\StateInterface
48 * @var \Drupal\Core\Extension\ModuleHandlerInterface
50 protected $moduleHandler;
55 * @var \Drupal\Core\Session\AccountInterface
60 * The bare HTML page renderer.
62 * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
64 protected $bareHtmlPageRenderer;
74 * The post update registry.
76 * @var \Drupal\Core\Update\UpdateRegistry
78 protected $postUpdateRegistry;
81 * Constructs a new UpdateController.
85 * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
86 * The keyvalue expirable factory.
87 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
88 * A cache backend interface.
89 * @param \Drupal\Core\State\StateInterface $state
91 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
93 * @param \Drupal\Core\Session\AccountInterface $account
95 * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
96 * The bare HTML page renderer.
97 * @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
98 * The post update registry.
100 public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, UpdateRegistry $post_update_registry) {
102 $this->keyValueExpirableFactory = $key_value_expirable_factory;
103 $this->cache = $cache;
104 $this->state = $state;
105 $this->moduleHandler = $module_handler;
106 $this->account = $account;
107 $this->bareHtmlPageRenderer = $bare_html_page_renderer;
108 $this->postUpdateRegistry = $post_update_registry;
114 public static function create(ContainerInterface $container) {
116 $container->get('app.root'),
117 $container->get('keyvalue.expirable'),
118 $container->get('cache.default'),
119 $container->get('state'),
120 $container->get('module_handler'),
121 $container->get('current_user'),
122 $container->get('bare_html_page_renderer'),
123 $container->get('update.post_update_registry')
128 * Returns a database update page.
131 * The update operation to perform. Can be any of the below:
136 * @param \Symfony\Component\HttpFoundation\Request $request
137 * The current request object.
139 * @return \Symfony\Component\HttpFoundation\Response
140 * A response object object.
142 public function handle($op, Request $request) {
143 require_once $this->root . '/core/includes/install.inc';
144 require_once $this->root . '/core/includes/update.inc';
146 drupal_load_updates();
147 update_fix_compatibility();
149 if ($request->query->get('continue')) {
150 $_SESSION['update_ignore_warnings'] = TRUE;
154 $requirements = update_check_requirements();
155 $severity = drupal_requirements_severity($requirements);
156 if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($_SESSION['update_ignore_warnings']))) {
157 $regions['sidebar_first'] = $this->updateTasksList('requirements');
158 $output = $this->requirements($severity, $requirements, $request);
163 $regions['sidebar_first'] = $this->updateTasksList('selection');
164 $output = $this->selection($request);
168 $regions['sidebar_first'] = $this->updateTasksList('run');
169 $output = $this->triggerBatch($request);
173 $regions['sidebar_first'] = $this->updateTasksList('info');
174 $output = $this->info($request);
178 $regions['sidebar_first'] = $this->updateTasksList('results');
179 $output = $this->results($request);
182 // Regular batch ops : defer to batch processing API.
184 require_once $this->root . '/core/includes/batch.inc';
185 $regions['sidebar_first'] = $this->updateTasksList('run');
186 $output = _batch_page($request);
191 if ($output instanceof Response) {
194 $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update');
196 return $this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions);
200 * Returns the info database update page.
202 * @param \Symfony\Component\HttpFoundation\Request $request
203 * The current request.
208 protected function info(Request $request) {
209 // Change query-strings on css/js files to enforce reload for all users.
210 _drupal_flush_css_js();
211 // Flush the cache of all data for the update status module.
212 $this->keyValueExpirableFactory->get('update')->deleteAll();
213 $this->keyValueExpirableFactory->get('update_available_release')->deleteAll();
215 $build['info_header'] = [
216 '#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="https://www.drupal.org/upgrade">upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.') . '</p>',
219 $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.");
220 $info[] = $this->t('Put your site into <a href=":url">maintenance mode</a>.', [
221 ':url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
223 $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.');
224 $info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.');
226 '#theme' => 'item_list',
227 '#list_type' => 'ol',
230 $build['info_footer'] = [
231 '#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
236 '#title' => $this->t('Continue'),
237 '#attributes' => ['class' => ['button', 'button--primary']],
238 // @todo Revisit once https://www.drupal.org/node/2548095 is in.
239 '#url' => Url::fromUri('base://selection'),
245 * Renders a list of available database updates.
247 * @param \Symfony\Component\HttpFoundation\Request $request
248 * The current request.
253 protected function selection(Request $request) {
254 // Make sure there is no stale theme registry.
255 $this->cache->deleteAll();
258 $incompatible_count = 0;
261 '#type' => 'details',
264 // Ensure system.module's updates appear first.
265 $build['start']['system'] = [];
267 $starting_updates = [];
268 $incompatible_updates_exist = FALSE;
269 $updates_per_module = [];
270 foreach (['update', 'post_update'] as $update_type) {
271 switch ($update_type) {
273 $updates = update_get_update_list();
276 $updates = $this->postUpdateRegistry->getPendingUpdateInformation();
279 foreach ($updates as $module => $update) {
280 if (!isset($update['start'])) {
281 $build['start'][$module] = [
283 '#title' => $module . ' module',
284 '#markup' => $update['warning'],
285 '#prefix' => '<div class="messages messages--warning">',
286 '#suffix' => '</div>',
288 $incompatible_updates_exist = TRUE;
291 if (!empty($update['pending'])) {
292 $updates_per_module += [$module => []];
293 $updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']);
294 $build['start'][$module] = [
296 '#value' => $update['start'],
298 // Store the previous items in order to merge normal updates and
299 // post_update functions together.
300 $build['start'][$module] = [
301 '#theme' => 'item_list',
302 '#items' => $updates_per_module[$module],
303 '#title' => $module . ' module',
306 if ($update_type === 'update') {
307 $starting_updates[$module] = $update['start'];
310 if (isset($update['pending'])) {
311 $count = $count + count($update['pending']);
316 // Find and label any incompatible updates.
317 foreach (update_resolve_dependencies($starting_updates) as $data) {
318 if (!$data['allowed']) {
319 $incompatible_updates_exist = TRUE;
320 $incompatible_count++;
321 $module_update_key = $data['module'] . '_updates';
322 if (isset($build['start'][$module_update_key]['#items'][$data['number']])) {
323 if ($data['missing_dependencies']) {
324 $text = $this->t('This update will been skipped due to the following missing dependencies:') . '<em>' . implode(', ', $data['missing_dependencies']) . '</em>';
327 $text = $this->t("This update will be skipped due to an error in the module's code.");
329 $build['start'][$module_update_key]['#items'][$data['number']] .= '<div class="warning">' . $text . '</div>';
331 // Move the module containing this update to the top of the list.
332 $build['start'] = [$module_update_key => $build['start'][$module_update_key]] + $build['start'];
336 // Warn the user if any updates were incompatible.
337 if ($incompatible_updates_exist) {
338 $this->messenger()->addWarning($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'));
342 $this->messenger()->addStatus($this->t('No pending updates.'));
346 '#links' => $this->helpfulLinks($request),
349 // No updates to run, so caches won't get flushed later. Clear them now.
350 drupal_flush_all_caches();
354 '#markup' => '<p>' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '</p>',
357 if ($incompatible_count) {
358 $build['start']['#title'] = $this->formatPlural(
360 '1 pending update (@number_applied to be applied, @number_incompatible skipped)',
361 '@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
362 ['@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count]
366 $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
368 // @todo Simplify with https://www.drupal.org/node/2548095
369 $base_url = str_replace('/update.php', '', $request->getBaseUrl());
370 $url = (new Url('system.db_update', ['op' => 'run']))->setOption('base_url', $base_url);
373 '#title' => $this->t('Apply pending updates'),
374 '#attributes' => ['class' => ['button', 'button--primary']],
377 '#access' => $url->access($this->currentUser()),
385 * Displays results of the update script with any accompanying errors.
387 * @param \Symfony\Component\HttpFoundation\Request $request
388 * The current request.
393 protected function results(Request $request) {
394 // @todo Simplify with https://www.drupal.org/node/2548095
395 $base_url = str_replace('/update.php', '', $request->getBaseUrl());
397 // Report end result.
398 $dblog_exists = $this->moduleHandler->moduleExists('dblog');
399 if ($dblog_exists && $this->account->hasPermission('access site reports')) {
400 $log_message = $this->t('All errors have been <a href=":url">logged</a>.', [
401 ':url' => Url::fromRoute('dblog.overview')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl(),
405 $log_message = $this->t('All errors have been logged.');
408 if (!empty($_SESSION['update_success'])) {
409 $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.', [':url' => Url::fromRoute('<front>')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl()]) . ' ' . $log_message . '</p>';
412 $last = reset($_SESSION['updates_remaining']);
413 list($module, $version) = array_pop($last);
414 $message = '<p class="error">' . $this->t('The update process was aborted prematurely while running <strong>update #@version in @module.module</strong>.', [
415 '@version' => $version,
416 '@module' => $module,
417 ]) . ' ' . $log_message;
419 $message .= ' ' . $this->t('You may need to check the <code>watchdog</code> database table manually.');
424 if (Settings::get('update_free_access')) {
425 $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>';
428 $build['message'] = [
429 '#markup' => $message,
433 '#links' => $this->helpfulLinks($request),
436 // Output a list of info messages.
437 if (!empty($_SESSION['update_results'])) {
439 foreach ($_SESSION['update_results'] as $module => $updates) {
440 if ($module != '#abort') {
441 $module_has_message = FALSE;
443 foreach ($updates as $name => $queries) {
445 foreach ($queries as $query) {
446 // If there is no message for this update, don't show anything.
447 if (empty($query['query'])) {
451 if ($query['success']) {
453 '#wrapper_attributes' => ['class' => ['success']],
454 '#markup' => $query['query'],
459 '#wrapper_attributes' => ['class' => ['failure']],
460 '#markup' => '<strong>' . $this->t('Failed:') . '</strong> ' . $query['query'],
466 $module_has_message = TRUE;
467 if (is_numeric($name)) {
468 $title = $this->t('Update #@count', ['@count' => $name]);
471 $title = $this->t('Update @name', ['@name' => trim($name, '_')]);
474 '#theme' => 'item_list',
475 '#items' => $messages,
481 // If there were any messages then prefix them with the module name
482 // and add it to the global message list.
483 if ($module_has_message) {
485 '#type' => 'container',
486 '#prefix' => '<h3>' . $this->t('@module module', ['@module' => $module]) . '</h3>',
487 '#children' => $info_messages,
493 $build['query_messages'] = [
494 '#type' => 'container',
495 '#children' => $all_messages,
496 '#attributes' => ['class' => ['update-results']],
497 '#prefix' => '<h2>' . $this->t('The following updates returned messages:') . '</h2>',
501 unset($_SESSION['update_results']);
502 unset($_SESSION['update_success']);
503 unset($_SESSION['update_ignore_warnings']);
509 * Renders a list of requirement errors or warnings.
511 * @param \Symfony\Component\HttpFoundation\Request $request
512 * The current request.
517 public function requirements($severity, array $requirements, Request $request) {
518 $options = $severity == REQUIREMENT_WARNING ? ['continue' => 1] : [];
519 // @todo Revisit once https://www.drupal.org/node/2548095 is in. Something
520 // like Url::fromRoute('system.db_update')->setOptions() should then be
522 $try_again_url = Url::fromUri($request->getUriForPath(''))->setOptions(['query' => $options])->toString(TRUE)->getGeneratedUrl();
524 $build['status_report'] = [
525 '#type' => 'status_report',
526 '#requirements' => $requirements,
527 '#suffix' => $this->t('Check the messages and <a href=":url">try again</a>.', [':url' => $try_again_url]),
530 $build['#title'] = $this->t('Requirements problem');
535 * Provides the update task list render array.
537 * @param string $active
539 * Can be one of 'requirements', 'info', 'selection', 'run', 'results'.
544 protected function updateTasksList($active = NULL) {
545 // Default list of tasks.
547 'requirements' => $this->t('Verify requirements'),
548 'info' => $this->t('Overview'),
549 'selection' => $this->t('Review updates'),
550 'run' => $this->t('Run updates'),
551 'results' => $this->t('Review log'),
555 '#theme' => 'maintenance_task_list',
557 '#active' => $active,
563 * Starts the database update batch process.
565 * @param \Symfony\Component\HttpFoundation\Request $request
566 * The current request object.
568 protected function triggerBatch(Request $request) {
569 $maintenance_mode = $this->state->get('system.maintenance_mode', FALSE);
570 // Store the current maintenance mode status in the session so that it can
571 // be restored at the end of the batch.
572 $_SESSION['maintenance_mode'] = $maintenance_mode;
573 // During the update, always put the site into maintenance mode so that
574 // in-progress schema changes do not affect visiting users.
575 if (empty($maintenance_mode)) {
576 $this->state->set('system.maintenance_mode', TRUE);
581 // Resolve any update dependencies to determine the actual updates that will
582 // be run and the order they will be run in.
583 $start = $this->getModuleUpdates();
584 $updates = update_resolve_dependencies($start);
586 // Store the dependencies for each update function in an array which the
587 // batch API can pass in to the batch operation each time it is called. (We
588 // do not store the entire update dependency array here because it is
589 // potentially very large.)
590 $dependency_map = [];
591 foreach ($updates as $function => $update) {
592 $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
595 // Determine updates to be performed.
596 foreach ($updates as $function => $update) {
597 if ($update['allowed']) {
598 // Set the installed version of each module so updates will start at the
599 // correct place. (The updates are already sorted, so we can simply base
600 // this on the first one we come across in the above foreach loop.)
601 if (isset($start[$update['module']])) {
602 drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
603 unset($start[$update['module']]);
605 $operations[] = ['update_do_one', [$update['module'], $update['number'], $dependency_map[$function]]];
609 $post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions();
612 // Now we rebuild all caches and after that execute the hook_post_update()
614 $operations[] = ['drupal_flush_all_caches', []];
615 foreach ($post_updates as $function) {
616 $operations[] = ['update_invoke_post_update', [$function]];
620 $batch['operations'] = $operations;
622 'title' => $this->t('Updating'),
623 'init_message' => $this->t('Starting updates'),
624 '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.'),
625 'finished' => ['\Drupal\system\Controller\DbUpdateController', 'batchFinished'],
629 // @todo Revisit once https://www.drupal.org/node/2548095 is in.
630 return batch_process(Url::fromUri('base://results'), Url::fromUri('base://start'));
634 * Finishes the update process and stores the results for eventual display.
636 * After the updates run, all caches are flushed. The update results are
637 * stored into the session (for example, to be displayed on the update results
638 * page in update.php). Additionally, if the site was off-line, now that the
639 * update process is completed, the site is set back online.
642 * Indicate that the batch API tasks were all completed successfully.
643 * @param array $results
644 * An array of all the results that were updated in update_do_one().
645 * @param array $operations
646 * A list of all the operations that had not been completed by the batch API.
648 public static function batchFinished($success, $results, $operations) {
649 // No updates to run, so caches won't get flushed later. Clear them now.
650 drupal_flush_all_caches();
652 $_SESSION['update_results'] = $results;
653 $_SESSION['update_success'] = $success;
654 $_SESSION['updates_remaining'] = $operations;
656 // Now that the update is done, we can put the site back online if it was
657 // previously not in maintenance mode.
658 if (empty($_SESSION['maintenance_mode'])) {
659 \Drupal::state()->set('system.maintenance_mode', FALSE);
661 unset($_SESSION['maintenance_mode']);
665 * Provides links to the homepage and administration pages.
667 * @param \Symfony\Component\HttpFoundation\Request $request
668 * The current request.
673 protected function helpfulLinks(Request $request) {
674 // @todo Simplify with https://www.drupal.org/node/2548095
675 $base_url = str_replace('/update.php', '', $request->getBaseUrl());
677 'title' => $this->t('Front page'),
678 'url' => Url::fromRoute('<front>')->setOption('base_url', $base_url),
680 if ($this->account->hasPermission('access administration pages')) {
681 $links['admin-pages'] = [
682 'title' => $this->t('Administration pages'),
683 'url' => Url::fromRoute('system.admin')->setOption('base_url', $base_url),
690 * Retrieves module updates.
693 * The module updates that can be performed.
695 protected function getModuleUpdates() {
697 $updates = update_get_update_list();
698 foreach ($updates as $module => $update) {
699 $return[$module] = $update['start'];