annotate core/modules/system/src/Controller/DbUpdateController.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\system\Controller;
Chris@0 4
Chris@0 5 use Drupal\Core\Cache\CacheBackendInterface;
Chris@0 6 use Drupal\Core\Controller\ControllerBase;
Chris@0 7 use Drupal\Core\Extension\ModuleHandlerInterface;
Chris@0 8 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
Chris@0 9 use Drupal\Core\Render\BareHtmlPageRendererInterface;
Chris@0 10 use Drupal\Core\Session\AccountInterface;
Chris@0 11 use Drupal\Core\Site\Settings;
Chris@0 12 use Drupal\Core\State\StateInterface;
Chris@0 13 use Drupal\Core\Update\UpdateRegistry;
Chris@0 14 use Drupal\Core\Url;
Chris@0 15 use Symfony\Component\DependencyInjection\ContainerInterface;
Chris@0 16 use Symfony\Component\HttpFoundation\Response;
Chris@0 17 use Symfony\Component\HttpFoundation\Request;
Chris@0 18
Chris@0 19 /**
Chris@0 20 * Controller routines for database update routes.
Chris@0 21 */
Chris@0 22 class DbUpdateController extends ControllerBase {
Chris@0 23
Chris@0 24 /**
Chris@0 25 * The keyvalue expirable factory.
Chris@0 26 *
Chris@0 27 * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
Chris@0 28 */
Chris@0 29 protected $keyValueExpirableFactory;
Chris@0 30
Chris@0 31 /**
Chris@0 32 * A cache backend interface.
Chris@0 33 *
Chris@0 34 * @var \Drupal\Core\Cache\CacheBackendInterface
Chris@0 35 */
Chris@0 36 protected $cache;
Chris@0 37
Chris@0 38 /**
Chris@0 39 * The state service.
Chris@0 40 *
Chris@0 41 * @var \Drupal\Core\State\StateInterface
Chris@0 42 */
Chris@0 43 protected $state;
Chris@0 44
Chris@0 45 /**
Chris@0 46 * The module handler.
Chris@0 47 *
Chris@0 48 * @var \Drupal\Core\Extension\ModuleHandlerInterface
Chris@0 49 */
Chris@0 50 protected $moduleHandler;
Chris@0 51
Chris@0 52 /**
Chris@0 53 * The current user.
Chris@0 54 *
Chris@0 55 * @var \Drupal\Core\Session\AccountInterface
Chris@0 56 */
Chris@0 57 protected $account;
Chris@0 58
Chris@0 59 /**
Chris@0 60 * The bare HTML page renderer.
Chris@0 61 *
Chris@0 62 * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
Chris@0 63 */
Chris@0 64 protected $bareHtmlPageRenderer;
Chris@0 65
Chris@0 66 /**
Chris@0 67 * The app root.
Chris@0 68 *
Chris@0 69 * @var string
Chris@0 70 */
Chris@0 71 protected $root;
Chris@0 72
Chris@0 73 /**
Chris@0 74 * The post update registry.
Chris@0 75 *
Chris@0 76 * @var \Drupal\Core\Update\UpdateRegistry
Chris@0 77 */
Chris@0 78 protected $postUpdateRegistry;
Chris@0 79
Chris@0 80 /**
Chris@0 81 * Constructs a new UpdateController.
Chris@0 82 *
Chris@0 83 * @param string $root
Chris@0 84 * The app root.
Chris@0 85 * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
Chris@0 86 * The keyvalue expirable factory.
Chris@0 87 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
Chris@0 88 * A cache backend interface.
Chris@0 89 * @param \Drupal\Core\State\StateInterface $state
Chris@0 90 * The state service.
Chris@0 91 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
Chris@0 92 * The module handler.
Chris@0 93 * @param \Drupal\Core\Session\AccountInterface $account
Chris@0 94 * The current user.
Chris@0 95 * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
Chris@0 96 * The bare HTML page renderer.
Chris@0 97 * @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
Chris@0 98 * The post update registry.
Chris@0 99 */
Chris@0 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) {
Chris@0 101 $this->root = $root;
Chris@0 102 $this->keyValueExpirableFactory = $key_value_expirable_factory;
Chris@0 103 $this->cache = $cache;
Chris@0 104 $this->state = $state;
Chris@0 105 $this->moduleHandler = $module_handler;
Chris@0 106 $this->account = $account;
Chris@0 107 $this->bareHtmlPageRenderer = $bare_html_page_renderer;
Chris@0 108 $this->postUpdateRegistry = $post_update_registry;
Chris@0 109 }
Chris@0 110
Chris@0 111 /**
Chris@0 112 * {@inheritdoc}
Chris@0 113 */
Chris@0 114 public static function create(ContainerInterface $container) {
Chris@0 115 return new static(
Chris@0 116 $container->get('app.root'),
Chris@0 117 $container->get('keyvalue.expirable'),
Chris@0 118 $container->get('cache.default'),
Chris@0 119 $container->get('state'),
Chris@0 120 $container->get('module_handler'),
Chris@0 121 $container->get('current_user'),
Chris@0 122 $container->get('bare_html_page_renderer'),
Chris@0 123 $container->get('update.post_update_registry')
Chris@0 124 );
Chris@0 125 }
Chris@0 126
Chris@0 127 /**
Chris@0 128 * Returns a database update page.
Chris@0 129 *
Chris@0 130 * @param string $op
Chris@0 131 * The update operation to perform. Can be any of the below:
Chris@0 132 * - info
Chris@0 133 * - selection
Chris@0 134 * - run
Chris@0 135 * - results
Chris@0 136 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 137 * The current request object.
Chris@0 138 *
Chris@0 139 * @return \Symfony\Component\HttpFoundation\Response
Chris@0 140 * A response object object.
Chris@0 141 */
Chris@0 142 public function handle($op, Request $request) {
Chris@0 143 require_once $this->root . '/core/includes/install.inc';
Chris@0 144 require_once $this->root . '/core/includes/update.inc';
Chris@0 145
Chris@0 146 drupal_load_updates();
Chris@0 147 update_fix_compatibility();
Chris@0 148
Chris@0 149 if ($request->query->get('continue')) {
Chris@0 150 $_SESSION['update_ignore_warnings'] = TRUE;
Chris@0 151 }
Chris@0 152
Chris@0 153 $regions = [];
Chris@0 154 $requirements = update_check_requirements();
Chris@0 155 $severity = drupal_requirements_severity($requirements);
Chris@0 156 if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($_SESSION['update_ignore_warnings']))) {
Chris@0 157 $regions['sidebar_first'] = $this->updateTasksList('requirements');
Chris@0 158 $output = $this->requirements($severity, $requirements, $request);
Chris@0 159 }
Chris@0 160 else {
Chris@0 161 switch ($op) {
Chris@0 162 case 'selection':
Chris@0 163 $regions['sidebar_first'] = $this->updateTasksList('selection');
Chris@0 164 $output = $this->selection($request);
Chris@0 165 break;
Chris@0 166
Chris@0 167 case 'run':
Chris@0 168 $regions['sidebar_first'] = $this->updateTasksList('run');
Chris@0 169 $output = $this->triggerBatch($request);
Chris@0 170 break;
Chris@0 171
Chris@0 172 case 'info':
Chris@0 173 $regions['sidebar_first'] = $this->updateTasksList('info');
Chris@0 174 $output = $this->info($request);
Chris@0 175 break;
Chris@0 176
Chris@0 177 case 'results':
Chris@0 178 $regions['sidebar_first'] = $this->updateTasksList('results');
Chris@0 179 $output = $this->results($request);
Chris@0 180 break;
Chris@0 181
Chris@0 182 // Regular batch ops : defer to batch processing API.
Chris@0 183 default:
Chris@0 184 require_once $this->root . '/core/includes/batch.inc';
Chris@0 185 $regions['sidebar_first'] = $this->updateTasksList('run');
Chris@0 186 $output = _batch_page($request);
Chris@0 187 break;
Chris@0 188 }
Chris@0 189 }
Chris@0 190
Chris@0 191 if ($output instanceof Response) {
Chris@0 192 return $output;
Chris@0 193 }
Chris@0 194 $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update');
Chris@0 195
Chris@0 196 return $this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions);
Chris@0 197 }
Chris@0 198
Chris@0 199 /**
Chris@0 200 * Returns the info database update page.
Chris@0 201 *
Chris@0 202 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 203 * The current request.
Chris@0 204 *
Chris@0 205 * @return array
Chris@0 206 * A render array.
Chris@0 207 */
Chris@0 208 protected function info(Request $request) {
Chris@0 209 // Change query-strings on css/js files to enforce reload for all users.
Chris@0 210 _drupal_flush_css_js();
Chris@0 211 // Flush the cache of all data for the update status module.
Chris@0 212 $this->keyValueExpirableFactory->get('update')->deleteAll();
Chris@0 213 $this->keyValueExpirableFactory->get('update_available_release')->deleteAll();
Chris@0 214
Chris@0 215 $build['info_header'] = [
Chris@0 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>',
Chris@0 217 ];
Chris@0 218
Chris@0 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.");
Chris@0 220 $info[] = $this->t('Put your site into <a href=":url">maintenance mode</a>.', [
Chris@0 221 ':url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
Chris@0 222 ]);
Chris@0 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.');
Chris@0 224 $info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.');
Chris@0 225 $build['info'] = [
Chris@0 226 '#theme' => 'item_list',
Chris@0 227 '#list_type' => 'ol',
Chris@0 228 '#items' => $info,
Chris@0 229 ];
Chris@0 230 $build['info_footer'] = [
Chris@0 231 '#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
Chris@0 232 ];
Chris@0 233
Chris@0 234 $build['link'] = [
Chris@0 235 '#type' => 'link',
Chris@0 236 '#title' => $this->t('Continue'),
Chris@0 237 '#attributes' => ['class' => ['button', 'button--primary']],
Chris@0 238 // @todo Revisit once https://www.drupal.org/node/2548095 is in.
Chris@0 239 '#url' => Url::fromUri('base://selection'),
Chris@0 240 ];
Chris@0 241 return $build;
Chris@0 242 }
Chris@0 243
Chris@0 244 /**
Chris@0 245 * Renders a list of available database updates.
Chris@0 246 *
Chris@0 247 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 248 * The current request.
Chris@0 249 *
Chris@0 250 * @return array
Chris@0 251 * A render array.
Chris@0 252 */
Chris@0 253 protected function selection(Request $request) {
Chris@0 254 // Make sure there is no stale theme registry.
Chris@0 255 $this->cache->deleteAll();
Chris@0 256
Chris@0 257 $count = 0;
Chris@0 258 $incompatible_count = 0;
Chris@0 259 $build['start'] = [
Chris@0 260 '#tree' => TRUE,
Chris@0 261 '#type' => 'details',
Chris@0 262 ];
Chris@0 263
Chris@0 264 // Ensure system.module's updates appear first.
Chris@0 265 $build['start']['system'] = [];
Chris@0 266
Chris@0 267 $starting_updates = [];
Chris@0 268 $incompatible_updates_exist = FALSE;
Chris@0 269 $updates_per_module = [];
Chris@0 270 foreach (['update', 'post_update'] as $update_type) {
Chris@0 271 switch ($update_type) {
Chris@0 272 case 'update':
Chris@0 273 $updates = update_get_update_list();
Chris@0 274 break;
Chris@0 275 case 'post_update':
Chris@0 276 $updates = $this->postUpdateRegistry->getPendingUpdateInformation();
Chris@0 277 break;
Chris@0 278 }
Chris@0 279 foreach ($updates as $module => $update) {
Chris@0 280 if (!isset($update['start'])) {
Chris@0 281 $build['start'][$module] = [
Chris@0 282 '#type' => 'item',
Chris@0 283 '#title' => $module . ' module',
Chris@0 284 '#markup' => $update['warning'],
Chris@0 285 '#prefix' => '<div class="messages messages--warning">',
Chris@0 286 '#suffix' => '</div>',
Chris@0 287 ];
Chris@0 288 $incompatible_updates_exist = TRUE;
Chris@0 289 continue;
Chris@0 290 }
Chris@0 291 if (!empty($update['pending'])) {
Chris@0 292 $updates_per_module += [$module => []];
Chris@0 293 $updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']);
Chris@0 294 $build['start'][$module] = [
Chris@0 295 '#type' => 'hidden',
Chris@0 296 '#value' => $update['start'],
Chris@0 297 ];
Chris@0 298 // Store the previous items in order to merge normal updates and
Chris@0 299 // post_update functions together.
Chris@0 300 $build['start'][$module] = [
Chris@0 301 '#theme' => 'item_list',
Chris@0 302 '#items' => $updates_per_module[$module],
Chris@0 303 '#title' => $module . ' module',
Chris@0 304 ];
Chris@0 305
Chris@0 306 if ($update_type === 'update') {
Chris@0 307 $starting_updates[$module] = $update['start'];
Chris@0 308 }
Chris@0 309 }
Chris@0 310 if (isset($update['pending'])) {
Chris@0 311 $count = $count + count($update['pending']);
Chris@0 312 }
Chris@0 313 }
Chris@0 314 }
Chris@0 315
Chris@0 316 // Find and label any incompatible updates.
Chris@0 317 foreach (update_resolve_dependencies($starting_updates) as $data) {
Chris@0 318 if (!$data['allowed']) {
Chris@0 319 $incompatible_updates_exist = TRUE;
Chris@0 320 $incompatible_count++;
Chris@0 321 $module_update_key = $data['module'] . '_updates';
Chris@0 322 if (isset($build['start'][$module_update_key]['#items'][$data['number']])) {
Chris@0 323 if ($data['missing_dependencies']) {
Chris@0 324 $text = $this->t('This update will been skipped due to the following missing dependencies:') . '<em>' . implode(', ', $data['missing_dependencies']) . '</em>';
Chris@0 325 }
Chris@0 326 else {
Chris@0 327 $text = $this->t("This update will be skipped due to an error in the module's code.");
Chris@0 328 }
Chris@0 329 $build['start'][$module_update_key]['#items'][$data['number']] .= '<div class="warning">' . $text . '</div>';
Chris@0 330 }
Chris@0 331 // Move the module containing this update to the top of the list.
Chris@0 332 $build['start'] = [$module_update_key => $build['start'][$module_update_key]] + $build['start'];
Chris@0 333 }
Chris@0 334 }
Chris@0 335
Chris@0 336 // Warn the user if any updates were incompatible.
Chris@0 337 if ($incompatible_updates_exist) {
Chris@17 338 $this->messenger()->addWarning($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'));
Chris@0 339 }
Chris@0 340
Chris@0 341 if (empty($count)) {
Chris@17 342 $this->messenger()->addStatus($this->t('No pending updates.'));
Chris@0 343 unset($build);
Chris@0 344 $build['links'] = [
Chris@0 345 '#theme' => 'links',
Chris@0 346 '#links' => $this->helpfulLinks($request),
Chris@0 347 ];
Chris@0 348
Chris@0 349 // No updates to run, so caches won't get flushed later. Clear them now.
Chris@0 350 drupal_flush_all_caches();
Chris@0 351 }
Chris@0 352 else {
Chris@0 353 $build['help'] = [
Chris@0 354 '#markup' => '<p>' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '</p>',
Chris@0 355 '#weight' => -5,
Chris@0 356 ];
Chris@0 357 if ($incompatible_count) {
Chris@0 358 $build['start']['#title'] = $this->formatPlural(
Chris@0 359 $count,
Chris@0 360 '1 pending update (@number_applied to be applied, @number_incompatible skipped)',
Chris@0 361 '@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
Chris@0 362 ['@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count]
Chris@0 363 );
Chris@0 364 }
Chris@0 365 else {
Chris@0 366 $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
Chris@0 367 }
Chris@0 368 // @todo Simplify with https://www.drupal.org/node/2548095
Chris@0 369 $base_url = str_replace('/update.php', '', $request->getBaseUrl());
Chris@0 370 $url = (new Url('system.db_update', ['op' => 'run']))->setOption('base_url', $base_url);
Chris@0 371 $build['link'] = [
Chris@0 372 '#type' => 'link',
Chris@0 373 '#title' => $this->t('Apply pending updates'),
Chris@0 374 '#attributes' => ['class' => ['button', 'button--primary']],
Chris@0 375 '#weight' => 5,
Chris@0 376 '#url' => $url,
Chris@0 377 '#access' => $url->access($this->currentUser()),
Chris@0 378 ];
Chris@0 379 }
Chris@0 380
Chris@0 381 return $build;
Chris@0 382 }
Chris@0 383
Chris@0 384 /**
Chris@0 385 * Displays results of the update script with any accompanying errors.
Chris@0 386 *
Chris@0 387 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 388 * The current request.
Chris@0 389 *
Chris@0 390 * @return array
Chris@0 391 * A render array.
Chris@0 392 */
Chris@0 393 protected function results(Request $request) {
Chris@0 394 // @todo Simplify with https://www.drupal.org/node/2548095
Chris@0 395 $base_url = str_replace('/update.php', '', $request->getBaseUrl());
Chris@0 396
Chris@0 397 // Report end result.
Chris@0 398 $dblog_exists = $this->moduleHandler->moduleExists('dblog');
Chris@0 399 if ($dblog_exists && $this->account->hasPermission('access site reports')) {
Chris@0 400 $log_message = $this->t('All errors have been <a href=":url">logged</a>.', [
Chris@0 401 ':url' => Url::fromRoute('dblog.overview')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl(),
Chris@0 402 ]);
Chris@0 403 }
Chris@0 404 else {
Chris@0 405 $log_message = $this->t('All errors have been logged.');
Chris@0 406 }
Chris@0 407
Chris@0 408 if (!empty($_SESSION['update_success'])) {
Chris@0 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>';
Chris@0 410 }
Chris@0 411 else {
Chris@0 412 $last = reset($_SESSION['updates_remaining']);
Chris@0 413 list($module, $version) = array_pop($last);
Chris@0 414 $message = '<p class="error">' . $this->t('The update process was aborted prematurely while running <strong>update #@version in @module.module</strong>.', [
Chris@0 415 '@version' => $version,
Chris@0 416 '@module' => $module,
Chris@0 417 ]) . ' ' . $log_message;
Chris@0 418 if ($dblog_exists) {
Chris@0 419 $message .= ' ' . $this->t('You may need to check the <code>watchdog</code> database table manually.');
Chris@0 420 }
Chris@0 421 $message .= '</p>';
Chris@0 422 }
Chris@0 423
Chris@0 424 if (Settings::get('update_free_access')) {
Chris@0 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>';
Chris@0 426 }
Chris@0 427
Chris@0 428 $build['message'] = [
Chris@0 429 '#markup' => $message,
Chris@0 430 ];
Chris@0 431 $build['links'] = [
Chris@0 432 '#theme' => 'links',
Chris@0 433 '#links' => $this->helpfulLinks($request),
Chris@0 434 ];
Chris@0 435
Chris@0 436 // Output a list of info messages.
Chris@0 437 if (!empty($_SESSION['update_results'])) {
Chris@0 438 $all_messages = [];
Chris@0 439 foreach ($_SESSION['update_results'] as $module => $updates) {
Chris@0 440 if ($module != '#abort') {
Chris@0 441 $module_has_message = FALSE;
Chris@0 442 $info_messages = [];
Chris@0 443 foreach ($updates as $name => $queries) {
Chris@0 444 $messages = [];
Chris@0 445 foreach ($queries as $query) {
Chris@0 446 // If there is no message for this update, don't show anything.
Chris@0 447 if (empty($query['query'])) {
Chris@0 448 continue;
Chris@0 449 }
Chris@0 450
Chris@0 451 if ($query['success']) {
Chris@0 452 $messages[] = [
Chris@0 453 '#wrapper_attributes' => ['class' => ['success']],
Chris@0 454 '#markup' => $query['query'],
Chris@0 455 ];
Chris@0 456 }
Chris@0 457 else {
Chris@0 458 $messages[] = [
Chris@0 459 '#wrapper_attributes' => ['class' => ['failure']],
Chris@0 460 '#markup' => '<strong>' . $this->t('Failed:') . '</strong> ' . $query['query'],
Chris@0 461 ];
Chris@0 462 }
Chris@0 463 }
Chris@0 464
Chris@0 465 if ($messages) {
Chris@0 466 $module_has_message = TRUE;
Chris@0 467 if (is_numeric($name)) {
Chris@0 468 $title = $this->t('Update #@count', ['@count' => $name]);
Chris@0 469 }
Chris@0 470 else {
Chris@0 471 $title = $this->t('Update @name', ['@name' => trim($name, '_')]);
Chris@0 472 }
Chris@0 473 $info_messages[] = [
Chris@0 474 '#theme' => 'item_list',
Chris@0 475 '#items' => $messages,
Chris@0 476 '#title' => $title,
Chris@0 477 ];
Chris@0 478 }
Chris@0 479 }
Chris@0 480
Chris@0 481 // If there were any messages then prefix them with the module name
Chris@0 482 // and add it to the global message list.
Chris@0 483 if ($module_has_message) {
Chris@0 484 $all_messages[] = [
Chris@0 485 '#type' => 'container',
Chris@0 486 '#prefix' => '<h3>' . $this->t('@module module', ['@module' => $module]) . '</h3>',
Chris@0 487 '#children' => $info_messages,
Chris@0 488 ];
Chris@0 489 }
Chris@0 490 }
Chris@0 491 }
Chris@0 492 if ($all_messages) {
Chris@0 493 $build['query_messages'] = [
Chris@0 494 '#type' => 'container',
Chris@0 495 '#children' => $all_messages,
Chris@0 496 '#attributes' => ['class' => ['update-results']],
Chris@0 497 '#prefix' => '<h2>' . $this->t('The following updates returned messages:') . '</h2>',
Chris@0 498 ];
Chris@0 499 }
Chris@0 500 }
Chris@0 501 unset($_SESSION['update_results']);
Chris@0 502 unset($_SESSION['update_success']);
Chris@0 503 unset($_SESSION['update_ignore_warnings']);
Chris@0 504
Chris@0 505 return $build;
Chris@0 506 }
Chris@0 507
Chris@0 508 /**
Chris@0 509 * Renders a list of requirement errors or warnings.
Chris@0 510 *
Chris@0 511 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 512 * The current request.
Chris@0 513 *
Chris@0 514 * @return array
Chris@0 515 * A render array.
Chris@0 516 */
Chris@0 517 public function requirements($severity, array $requirements, Request $request) {
Chris@0 518 $options = $severity == REQUIREMENT_WARNING ? ['continue' => 1] : [];
Chris@0 519 // @todo Revisit once https://www.drupal.org/node/2548095 is in. Something
Chris@0 520 // like Url::fromRoute('system.db_update')->setOptions() should then be
Chris@0 521 // possible.
Chris@0 522 $try_again_url = Url::fromUri($request->getUriForPath(''))->setOptions(['query' => $options])->toString(TRUE)->getGeneratedUrl();
Chris@0 523
Chris@0 524 $build['status_report'] = [
Chris@0 525 '#type' => 'status_report',
Chris@0 526 '#requirements' => $requirements,
Chris@17 527 '#suffix' => $this->t('Check the messages and <a href=":url">try again</a>.', [':url' => $try_again_url]),
Chris@0 528 ];
Chris@0 529
Chris@0 530 $build['#title'] = $this->t('Requirements problem');
Chris@0 531 return $build;
Chris@0 532 }
Chris@0 533
Chris@0 534 /**
Chris@0 535 * Provides the update task list render array.
Chris@0 536 *
Chris@0 537 * @param string $active
Chris@0 538 * The active task.
Chris@0 539 * Can be one of 'requirements', 'info', 'selection', 'run', 'results'.
Chris@0 540 *
Chris@0 541 * @return array
Chris@0 542 * A render array.
Chris@0 543 */
Chris@0 544 protected function updateTasksList($active = NULL) {
Chris@0 545 // Default list of tasks.
Chris@0 546 $tasks = [
Chris@0 547 'requirements' => $this->t('Verify requirements'),
Chris@0 548 'info' => $this->t('Overview'),
Chris@0 549 'selection' => $this->t('Review updates'),
Chris@0 550 'run' => $this->t('Run updates'),
Chris@0 551 'results' => $this->t('Review log'),
Chris@0 552 ];
Chris@0 553
Chris@0 554 $task_list = [
Chris@0 555 '#theme' => 'maintenance_task_list',
Chris@0 556 '#items' => $tasks,
Chris@0 557 '#active' => $active,
Chris@0 558 ];
Chris@0 559 return $task_list;
Chris@0 560 }
Chris@0 561
Chris@0 562 /**
Chris@0 563 * Starts the database update batch process.
Chris@0 564 *
Chris@0 565 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 566 * The current request object.
Chris@0 567 */
Chris@0 568 protected function triggerBatch(Request $request) {
Chris@0 569 $maintenance_mode = $this->state->get('system.maintenance_mode', FALSE);
Chris@0 570 // Store the current maintenance mode status in the session so that it can
Chris@0 571 // be restored at the end of the batch.
Chris@0 572 $_SESSION['maintenance_mode'] = $maintenance_mode;
Chris@0 573 // During the update, always put the site into maintenance mode so that
Chris@0 574 // in-progress schema changes do not affect visiting users.
Chris@0 575 if (empty($maintenance_mode)) {
Chris@0 576 $this->state->set('system.maintenance_mode', TRUE);
Chris@0 577 }
Chris@0 578
Chris@0 579 $operations = [];
Chris@0 580
Chris@0 581 // Resolve any update dependencies to determine the actual updates that will
Chris@0 582 // be run and the order they will be run in.
Chris@0 583 $start = $this->getModuleUpdates();
Chris@0 584 $updates = update_resolve_dependencies($start);
Chris@0 585
Chris@0 586 // Store the dependencies for each update function in an array which the
Chris@0 587 // batch API can pass in to the batch operation each time it is called. (We
Chris@0 588 // do not store the entire update dependency array here because it is
Chris@0 589 // potentially very large.)
Chris@0 590 $dependency_map = [];
Chris@0 591 foreach ($updates as $function => $update) {
Chris@0 592 $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
Chris@0 593 }
Chris@0 594
Chris@0 595 // Determine updates to be performed.
Chris@0 596 foreach ($updates as $function => $update) {
Chris@0 597 if ($update['allowed']) {
Chris@0 598 // Set the installed version of each module so updates will start at the
Chris@0 599 // correct place. (The updates are already sorted, so we can simply base
Chris@0 600 // this on the first one we come across in the above foreach loop.)
Chris@0 601 if (isset($start[$update['module']])) {
Chris@0 602 drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
Chris@0 603 unset($start[$update['module']]);
Chris@0 604 }
Chris@0 605 $operations[] = ['update_do_one', [$update['module'], $update['number'], $dependency_map[$function]]];
Chris@0 606 }
Chris@0 607 }
Chris@0 608
Chris@0 609 $post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions();
Chris@0 610
Chris@0 611 if ($post_updates) {
Chris@0 612 // Now we rebuild all caches and after that execute the hook_post_update()
Chris@0 613 // functions.
Chris@0 614 $operations[] = ['drupal_flush_all_caches', []];
Chris@0 615 foreach ($post_updates as $function) {
Chris@0 616 $operations[] = ['update_invoke_post_update', [$function]];
Chris@0 617 }
Chris@0 618 }
Chris@0 619
Chris@0 620 $batch['operations'] = $operations;
Chris@0 621 $batch += [
Chris@0 622 'title' => $this->t('Updating'),
Chris@0 623 'init_message' => $this->t('Starting updates'),
Chris@0 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.'),
Chris@0 625 'finished' => ['\Drupal\system\Controller\DbUpdateController', 'batchFinished'],
Chris@0 626 ];
Chris@0 627 batch_set($batch);
Chris@0 628
Chris@0 629 // @todo Revisit once https://www.drupal.org/node/2548095 is in.
Chris@0 630 return batch_process(Url::fromUri('base://results'), Url::fromUri('base://start'));
Chris@0 631 }
Chris@0 632
Chris@0 633 /**
Chris@0 634 * Finishes the update process and stores the results for eventual display.
Chris@0 635 *
Chris@0 636 * After the updates run, all caches are flushed. The update results are
Chris@0 637 * stored into the session (for example, to be displayed on the update results
Chris@0 638 * page in update.php). Additionally, if the site was off-line, now that the
Chris@0 639 * update process is completed, the site is set back online.
Chris@0 640 *
Chris@0 641 * @param $success
Chris@0 642 * Indicate that the batch API tasks were all completed successfully.
Chris@0 643 * @param array $results
Chris@0 644 * An array of all the results that were updated in update_do_one().
Chris@0 645 * @param array $operations
Chris@0 646 * A list of all the operations that had not been completed by the batch API.
Chris@0 647 */
Chris@0 648 public static function batchFinished($success, $results, $operations) {
Chris@0 649 // No updates to run, so caches won't get flushed later. Clear them now.
Chris@0 650 drupal_flush_all_caches();
Chris@0 651
Chris@0 652 $_SESSION['update_results'] = $results;
Chris@0 653 $_SESSION['update_success'] = $success;
Chris@0 654 $_SESSION['updates_remaining'] = $operations;
Chris@0 655
Chris@0 656 // Now that the update is done, we can put the site back online if it was
Chris@0 657 // previously not in maintenance mode.
Chris@0 658 if (empty($_SESSION['maintenance_mode'])) {
Chris@0 659 \Drupal::state()->set('system.maintenance_mode', FALSE);
Chris@0 660 }
Chris@0 661 unset($_SESSION['maintenance_mode']);
Chris@0 662 }
Chris@0 663
Chris@0 664 /**
Chris@0 665 * Provides links to the homepage and administration pages.
Chris@0 666 *
Chris@0 667 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 668 * The current request.
Chris@0 669 *
Chris@0 670 * @return array
Chris@0 671 * An array of links.
Chris@0 672 */
Chris@0 673 protected function helpfulLinks(Request $request) {
Chris@0 674 // @todo Simplify with https://www.drupal.org/node/2548095
Chris@0 675 $base_url = str_replace('/update.php', '', $request->getBaseUrl());
Chris@0 676 $links['front'] = [
Chris@0 677 'title' => $this->t('Front page'),
Chris@0 678 'url' => Url::fromRoute('<front>')->setOption('base_url', $base_url),
Chris@0 679 ];
Chris@0 680 if ($this->account->hasPermission('access administration pages')) {
Chris@0 681 $links['admin-pages'] = [
Chris@0 682 'title' => $this->t('Administration pages'),
Chris@0 683 'url' => Url::fromRoute('system.admin')->setOption('base_url', $base_url),
Chris@0 684 ];
Chris@0 685 }
Chris@0 686 return $links;
Chris@0 687 }
Chris@0 688
Chris@0 689 /**
Chris@0 690 * Retrieves module updates.
Chris@0 691 *
Chris@0 692 * @return array
Chris@0 693 * The module updates that can be performed.
Chris@0 694 */
Chris@0 695 protected function getModuleUpdates() {
Chris@0 696 $return = [];
Chris@0 697 $updates = update_get_update_list();
Chris@0 698 foreach ($updates as $module => $update) {
Chris@0 699 $return[$module] = $update['start'];
Chris@0 700 }
Chris@0 701
Chris@0 702 return $return;
Chris@0 703 }
Chris@0 704
Chris@0 705 }