123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705 |
- <?php
- 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\Render\BareHtmlPageRendererInterface;
- use Drupal\Core\Session\AccountInterface;
- use Drupal\Core\Site\Settings;
- use Drupal\Core\State\StateInterface;
- use Drupal\Core\Update\UpdateRegistry;
- 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;
- /**
- * The bare HTML page renderer.
- *
- * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
- */
- protected $bareHtmlPageRenderer;
- /**
- * The app root.
- *
- * @var string
- */
- protected $root;
- /**
- * The post update registry.
- *
- * @var \Drupal\Core\Update\UpdateRegistry
- */
- protected $postUpdateRegistry;
- /**
- * Constructs a new UpdateController.
- *
- * @param string $root
- * The app root.
- * @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.
- * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
- * The bare HTML page renderer.
- * @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
- * The post update registry.
- */
- 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) {
- $this->root = $root;
- $this->keyValueExpirableFactory = $key_value_expirable_factory;
- $this->cache = $cache;
- $this->state = $state;
- $this->moduleHandler = $module_handler;
- $this->account = $account;
- $this->bareHtmlPageRenderer = $bare_html_page_renderer;
- $this->postUpdateRegistry = $post_update_registry;
- }
- /**
- * {@inheritdoc}
- */
- public static function create(ContainerInterface $container) {
- return new static(
- $container->get('app.root'),
- $container->get('keyvalue.expirable'),
- $container->get('cache.default'),
- $container->get('state'),
- $container->get('module_handler'),
- $container->get('current_user'),
- $container->get('bare_html_page_renderer'),
- $container->get('update.post_update_registry')
- );
- }
- /**
- * 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 $this->root . '/core/includes/install.inc';
- require_once $this->root . '/core/includes/update.inc';
- drupal_load_updates();
- update_fix_compatibility();
- if ($request->query->get('continue')) {
- $_SESSION['update_ignore_warnings'] = TRUE;
- }
- $regions = [];
- $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, $request);
- }
- else {
- switch ($op) {
- case 'selection':
- $regions['sidebar_first'] = $this->updateTasksList('selection');
- $output = $this->selection($request);
- 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($request);
- break;
- case 'results':
- $regions['sidebar_first'] = $this->updateTasksList('results');
- $output = $this->results($request);
- break;
- // Regular batch ops : defer to batch processing API.
- default:
- require_once $this->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 $this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions);
- }
- /**
- * Returns the info database update page.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- *
- * @return array
- * A render array.
- */
- protected function info(Request $request) {
- // 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'] = [
- '#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>',
- ];
- $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>.', [
- ':url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
- ]);
- $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'] = [
- '#theme' => 'item_list',
- '#list_type' => 'ol',
- '#items' => $info,
- ];
- $build['info_footer'] = [
- '#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
- ];
- $build['link'] = [
- '#type' => 'link',
- '#title' => $this->t('Continue'),
- '#attributes' => ['class' => ['button', 'button--primary']],
- // @todo Revisit once https://www.drupal.org/node/2548095 is in.
- '#url' => Url::fromUri('base://selection'),
- ];
- return $build;
- }
- /**
- * Renders a list of available database updates.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- *
- * @return array
- * A render array.
- */
- protected function selection(Request $request) {
- // Make sure there is no stale theme registry.
- $this->cache->deleteAll();
- $count = 0;
- $incompatible_count = 0;
- $build['start'] = [
- '#tree' => TRUE,
- '#type' => 'details',
- ];
- // Ensure system.module's updates appear first.
- $build['start']['system'] = [];
- $starting_updates = [];
- $incompatible_updates_exist = FALSE;
- $updates_per_module = [];
- foreach (['update', 'post_update'] as $update_type) {
- switch ($update_type) {
- case 'update':
- $updates = update_get_update_list();
- break;
- case 'post_update':
- $updates = $this->postUpdateRegistry->getPendingUpdateInformation();
- break;
- }
- foreach ($updates as $module => $update) {
- if (!isset($update['start'])) {
- $build['start'][$module] = [
- '#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'])) {
- $updates_per_module += [$module => []];
- $updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']);
- $build['start'][$module] = [
- '#type' => 'hidden',
- '#value' => $update['start'],
- ];
- // Store the previous items in order to merge normal updates and
- // post_update functions together.
- $build['start'][$module] = [
- '#theme' => 'item_list',
- '#items' => $updates_per_module[$module],
- '#title' => $module . ' module',
- ];
- if ($update_type === 'update') {
- $starting_updates[$module] = $update['start'];
- }
- }
- 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'] = [$module_update_key => $build['start'][$module_update_key]] + $build['start'];
- }
- }
- // Warn the user if any updates were incompatible.
- if ($incompatible_updates_exist) {
- $this->messenger()->addWarning($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'));
- }
- if (empty($count)) {
- $this->messenger()->addStatus($this->t('No pending updates.'));
- unset($build);
- $build['links'] = [
- '#theme' => 'links',
- '#links' => $this->helpfulLinks($request),
- ];
- // No updates to run, so caches won't get flushed later. Clear them now.
- drupal_flush_all_caches();
- }
- else {
- $build['help'] = [
- '#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)',
- ['@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count]
- );
- }
- else {
- $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
- }
- // @todo Simplify with https://www.drupal.org/node/2548095
- $base_url = str_replace('/update.php', '', $request->getBaseUrl());
- $url = (new Url('system.db_update', ['op' => 'run']))->setOption('base_url', $base_url);
- $build['link'] = [
- '#type' => 'link',
- '#title' => $this->t('Apply pending updates'),
- '#attributes' => ['class' => ['button', 'button--primary']],
- '#weight' => 5,
- '#url' => $url,
- '#access' => $url->access($this->currentUser()),
- ];
- }
- return $build;
- }
- /**
- * Displays results of the update script with any accompanying errors.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- *
- * @return array
- * A render array.
- */
- protected function results(Request $request) {
- // @todo Simplify with https://www.drupal.org/node/2548095
- $base_url = str_replace('/update.php', '', $request->getBaseUrl());
- // 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>.', [
- ':url' => Url::fromRoute('dblog.overview')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl(),
- ]);
- }
- 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.', [':url' => Url::fromRoute('<front>')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl()]) . ' ' . $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>.', [
- '@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'] = [
- '#markup' => $message,
- ];
- $build['links'] = [
- '#theme' => 'links',
- '#links' => $this->helpfulLinks($request),
- ];
- // Output a list of info messages.
- if (!empty($_SESSION['update_results'])) {
- $all_messages = [];
- foreach ($_SESSION['update_results'] as $module => $updates) {
- if ($module != '#abort') {
- $module_has_message = FALSE;
- $info_messages = [];
- foreach ($updates as $name => $queries) {
- $messages = [];
- 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[] = [
- '#wrapper_attributes' => ['class' => ['success']],
- '#markup' => $query['query'],
- ];
- }
- else {
- $messages[] = [
- '#wrapper_attributes' => ['class' => ['failure']],
- '#markup' => '<strong>' . $this->t('Failed:') . '</strong> ' . $query['query'],
- ];
- }
- }
- if ($messages) {
- $module_has_message = TRUE;
- if (is_numeric($name)) {
- $title = $this->t('Update #@count', ['@count' => $name]);
- }
- else {
- $title = $this->t('Update @name', ['@name' => trim($name, '_')]);
- }
- $info_messages[] = [
- '#theme' => 'item_list',
- '#items' => $messages,
- '#title' => $title,
- ];
- }
- }
- // 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[] = [
- '#type' => 'container',
- '#prefix' => '<h3>' . $this->t('@module module', ['@module' => $module]) . '</h3>',
- '#children' => $info_messages,
- ];
- }
- }
- }
- if ($all_messages) {
- $build['query_messages'] = [
- '#type' => 'container',
- '#children' => $all_messages,
- '#attributes' => ['class' => ['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.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- *
- * @return array
- * A render array.
- */
- public function requirements($severity, array $requirements, Request $request) {
- $options = $severity == REQUIREMENT_WARNING ? ['continue' => 1] : [];
- // @todo Revisit once https://www.drupal.org/node/2548095 is in. Something
- // like Url::fromRoute('system.db_update')->setOptions() should then be
- // possible.
- $try_again_url = Url::fromUri($request->getUriForPath(''))->setOptions(['query' => $options])->toString(TRUE)->getGeneratedUrl();
- $build['status_report'] = [
- '#type' => 'status_report',
- '#requirements' => $requirements,
- '#suffix' => $this->t('Check the messages and <a href=":url">try again</a>.', [':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 = [
- '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 = [
- '#theme' => 'maintenance_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) {
- $maintenance_mode = $this->state->get('system.maintenance_mode', FALSE);
- // Store the current maintenance mode status in the session so that it can
- // be restored at the end of the batch.
- $_SESSION['maintenance_mode'] = $maintenance_mode;
- // During the update, always put the site into maintenance mode so that
- // in-progress schema changes do not affect visiting users.
- if (empty($maintenance_mode)) {
- $this->state->set('system.maintenance_mode', TRUE);
- }
- $operations = [];
- // Resolve any update dependencies to determine the actual updates that will
- // be run and the order they will be run in.
- $start = $this->getModuleUpdates();
- $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 = [];
- foreach ($updates as $function => $update) {
- $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
- }
- // Determine updates to be performed.
- foreach ($updates as $function => $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']]);
- }
- $operations[] = ['update_do_one', [$update['module'], $update['number'], $dependency_map[$function]]];
- }
- }
- $post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions();
- if ($post_updates) {
- // Now we rebuild all caches and after that execute the hook_post_update()
- // functions.
- $operations[] = ['drupal_flush_all_caches', []];
- foreach ($post_updates as $function) {
- $operations[] = ['update_invoke_post_update', [$function]];
- }
- }
- $batch['operations'] = $operations;
- $batch += [
- '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' => ['\Drupal\system\Controller\DbUpdateController', 'batchFinished'],
- ];
- batch_set($batch);
- // @todo Revisit once https://www.drupal.org/node/2548095 is in.
- return batch_process(Url::fromUri('base://results'), Url::fromUri('base://start'));
- }
- /**
- * 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 not in maintenance mode.
- if (empty($_SESSION['maintenance_mode'])) {
- \Drupal::state()->set('system.maintenance_mode', FALSE);
- }
- unset($_SESSION['maintenance_mode']);
- }
- /**
- * Provides links to the homepage and administration pages.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- *
- * @return array
- * An array of links.
- */
- protected function helpfulLinks(Request $request) {
- // @todo Simplify with https://www.drupal.org/node/2548095
- $base_url = str_replace('/update.php', '', $request->getBaseUrl());
- $links['front'] = [
- 'title' => $this->t('Front page'),
- 'url' => Url::fromRoute('<front>')->setOption('base_url', $base_url),
- ];
- if ($this->account->hasPermission('access administration pages')) {
- $links['admin-pages'] = [
- 'title' => $this->t('Administration pages'),
- 'url' => Url::fromRoute('system.admin')->setOption('base_url', $base_url),
- ];
- }
- return $links;
- }
- /**
- * Retrieves module updates.
- *
- * @return array
- * The module updates that can be performed.
- */
- protected function getModuleUpdates() {
- $return = [];
- $updates = update_get_update_list();
- foreach ($updates as $module => $update) {
- $return[$module] = $update['start'];
- }
- return $return;
- }
- }
|