View source
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Batch\BatchBuilder;
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;
class DbUpdateController extends ControllerBase {
protected $keyValueExpirableFactory;
protected $cache;
protected $state;
protected $moduleHandler;
protected $account;
protected $bareHtmlPageRenderer;
protected $root;
protected $postUpdateRegistry;
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;
}
public static function create(ContainerInterface $container) {
return new static($container
->getParameter('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'));
}
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();
if ($request->query
->get('continue')) {
$request
->getSession()
->set('update_ignore_warnings', TRUE);
}
$regions = [];
$requirements = update_check_requirements();
$severity = drupal_requirements_severity($requirements);
if ($severity == REQUIREMENT_ERROR || $severity == REQUIREMENT_WARNING && !$request
->getSession()
->has('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;
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 = $output['#title'] ?? $this
->t('Drupal database update');
return $this->bareHtmlPageRenderer
->renderBarePage($output, $title, 'maintenance_page', $regions);
}
protected function info(Request $request) {
_drupal_flush_css_js();
$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/docs/updating-drupal">Updating Drupal guide</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.");
$base_url = str_replace('/update.php', '', $request
->getBaseUrl());
$info[] = $this
->t('Put your site into <a href=":url">maintenance mode</a>.', [
':url' => Url::fromRoute('system.site_maintenance_mode')
->setOption('base_url', $base_url)
->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',
],
],
'#url' => Url::fromUri('base://selection'),
];
return $build;
}
protected function selection(Request $request) {
$this->cache
->deleteAll();
$count = 0;
$incompatible_count = 0;
$build['start'] = [
'#tree' => TRUE,
'#type' => 'details',
];
$build['start']['system'] = [];
$starting_updates = [];
$incompatible_updates_exist = FALSE;
$updates_per_extension = [];
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 $extension => $update) {
if (!isset($update['start'])) {
$build['start'][$extension] = [
'#type' => 'item',
'#title' => $extension . ($this->moduleHandler
->moduleExists($extension) ? ' module' : ' theme'),
'#markup' => $update['warning'],
'#prefix' => '<div class="messages messages--warning">',
'#suffix' => '</div>',
];
$incompatible_updates_exist = TRUE;
continue;
}
if (!empty($update['pending'])) {
$updates_per_extension += [
$extension => [],
];
$updates_per_extension[$extension] = array_merge($updates_per_extension[$extension], $update['pending']);
$build['start'][$extension] = [
'#type' => 'hidden',
'#value' => $update['start'],
];
$build['start'][$extension] = [
'#theme' => 'item_list',
'#items' => $updates_per_extension[$extension],
'#title' => $extension . ($this->moduleHandler
->moduleExists($extension) ? ' module' : ' theme'),
];
if ($update_type === 'update') {
$starting_updates[$extension] = $update['start'];
}
}
if (isset($update['pending'])) {
$count = $count + count($update['pending']);
}
}
}
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>';
}
$build['start'] = [
$module_update_key => $build['start'][$module_update_key],
] + $build['start'];
}
}
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),
];
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');
}
$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;
}
protected function results(Request $request) {
$base_url = str_replace('/update.php', '', $request
->getBaseUrl());
$session = $request
->getSession();
$update_results = $session
->remove('update_results');
$update_success = $session
->remove('update_success');
$session
->remove('update_ignore_warnings');
$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 ($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 = $session
->get('updates_remaining');
$last = reset($last);
[
$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),
];
if (!empty($update_results)) {
$all_messages = [];
foreach ($update_results as $extension => $updates) {
if ($extension != '#abort') {
$extension_has_message = FALSE;
$info_messages = [];
foreach ($updates as $name => $queries) {
$messages = [];
foreach ($queries as $query) {
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) {
$extension_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 ($extension_has_message) {
$header = $this->moduleHandler
->moduleExists($extension) ? $this
->t('@module module', [
'@module' => $extension,
]) : $this
->t('@theme theme', [
'@theme' => $extension,
]);
$all_messages[] = [
'#type' => 'container',
'#prefix' => '<h3>' . $header . '</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>',
];
}
}
return $build;
}
public function requirements($severity, array $requirements, Request $request) {
$options = $severity == REQUIREMENT_WARNING ? [
'continue' => 1,
] : [];
$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;
}
protected function updateTasksList($active = NULL) {
$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;
}
protected function triggerBatch(Request $request) {
$maintenance_mode = $this->state
->get('system.maintenance_mode', FALSE);
$request
->getSession()
->set('maintenance_mode', $maintenance_mode);
if (empty($maintenance_mode)) {
$this->state
->set('system.maintenance_mode', TRUE);
}
$batch_builder = (new BatchBuilder())
->setTitle($this
->t('Updating'))
->setInitMessage($this
->t('Starting updates'))
->setErrorMessage($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.'))
->setFinishCallback([
DbUpdateController::class,
'batchFinished',
]);
$start = $this
->getModuleUpdates();
$updates = update_resolve_dependencies($start);
$dependency_map = [];
foreach ($updates as $function => $update) {
$dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
}
foreach ($updates as $function => $update) {
if ($update['allowed']) {
if (isset($start[$update['module']])) {
\Drupal::service('update.update_hook_registry')
->setInstalledVersion($update['module'], $update['number'] - 1);
unset($start[$update['module']]);
}
$batch_builder
->addOperation('update_do_one', [
$update['module'],
$update['number'],
$dependency_map[$function],
]);
}
}
$post_updates = $this->postUpdateRegistry
->getPendingUpdateFunctions();
if ($post_updates) {
$batch_builder
->addOperation('drupal_flush_all_caches', []);
foreach ($post_updates as $function) {
$batch_builder
->addOperation('update_invoke_post_update', [
$function,
]);
}
}
batch_set($batch_builder
->toArray());
return batch_process(Url::fromUri('base://results'), Url::fromUri('base://start'));
}
public static function batchFinished($success, $results, $operations) {
drupal_flush_all_caches();
$session = \Drupal::request()
->getSession();
$session
->set('update_results', $results);
$session
->set('update_success', $success);
$session
->set('updates_remaining', $operations);
if (!$session
->remove('maintenance_mode')) {
\Drupal::state()
->set('system.maintenance_mode', FALSE);
}
}
protected function helpfulLinks(Request $request) {
$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;
}
protected function getModuleUpdates() {
$return = [];
$updates = update_get_update_list();
foreach ($updates as $module => $update) {
$return[$module] = $update['start'];
}
return $return;
}
}