DbUpdateController.php 23.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
<?php

/**
 * @file
 * Contains \Drupal\system\Controller\DbUpdateController.
 */

namespace Drupal\system\Controller;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
14
use Drupal\Core\Render\BareHtmlPageRendererInterface;
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

/**
 * Controller routines for database update routes.
 */
class DbUpdateController extends ControllerBase {

  /**
   * The keyvalue expirable factory.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
   */
  protected $keyValueExpirableFactory;

  /**
   * A cache backend interface.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * The state service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $account;

63 64 65 66 67 68 69
  /**
   * The bare HTML page renderer.
   *
   * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
   */
  protected $bareHtmlPageRenderer;

70 71 72 73 74 75 76
  /**
   * The app root.
   *
   * @var string
   */
  protected $root;

77 78 79
  /**
   * Constructs a new UpdateController.
   *
80 81
   * @param string $root
   *   The app root.
82 83 84 85 86 87 88 89 90 91
   * @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.
92 93
   * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
   *   The bare HTML page renderer.
94
   */
95
  public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) {
96
    $this->root = $root;
97 98 99 100 101
    $this->keyValueExpirableFactory = $key_value_expirable_factory;
    $this->cache = $cache;
    $this->state = $state;
    $this->moduleHandler = $module_handler;
    $this->account = $account;
102
    $this->bareHtmlPageRenderer = $bare_html_page_renderer;
103 104 105 106 107 108 109
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
110
      $container->get('app.root'),
111 112 113 114
      $container->get('keyvalue.expirable'),
      $container->get('cache.default'),
      $container->get('state'),
      $container->get('module_handler'),
115
      $container->get('current_user'),
116
      $container->get('bare_html_page_renderer')
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
    );
  }

  /**
   * 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) {
136 137
    require_once $this->root . '/core/includes/install.inc';
    require_once $this->root . '/core/includes/update.inc';
138 139 140 141 142 143 144 145 146 147 148 149 150

    drupal_load_updates();
    update_fix_compatibility();

    if ($request->query->get('continue')) {
      $_SESSION['update_ignore_warnings'] = TRUE;
    }

    $regions = array();
    $requirements = update_check_requirements();
    $severity = drupal_requirements_severity($requirements);
    if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($_SESSION['update_ignore_warnings']))) {
      $regions['sidebar_first'] = $this->updateTasksList('requirements');
151
      $output = $this->requirements($severity, $requirements, $request);
152 153 154 155 156
    }
    else {
      switch ($op) {
        case 'selection':
          $regions['sidebar_first'] = $this->updateTasksList('selection');
157
          $output = $this->selection($request);
158 159 160 161 162 163 164 165 166
          break;

        case 'run':
          $regions['sidebar_first'] = $this->updateTasksList('run');
          $output = $this->triggerBatch($request);
          break;

        case 'info':
          $regions['sidebar_first'] = $this->updateTasksList('info');
167
          $output = $this->info($request);
168 169 170 171
          break;

        case 'results':
          $regions['sidebar_first'] = $this->updateTasksList('results');
172
          $output = $this->results($request);
173 174 175 176
          break;

        // Regular batch ops : defer to batch processing API.
        default:
177
          require_once $this->root . '/core/includes/batch.inc';
178 179 180 181 182 183 184 185 186 187 188
          $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');

189
    return $this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions);
190 191 192 193 194
  }

  /**
   * Returns the info database update page.
   *
195 196 197
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
198 199 200
   * @return array
   *   A render array.
   */
201
  protected function info(Request $request) {
202 203 204 205 206 207 208
    // Change query-strings on css/js files to enforce reload for all users.
    _drupal_flush_css_js();
    // Flush the cache of all data for the update status module.
    $this->keyValueExpirableFactory->get('update')->deleteAll();
    $this->keyValueExpirableFactory->get('update_available_release')->deleteAll();

    $build['info_header'] = array(
209
      '#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>',
210 211 212 213
    );

    $info[] = $this->t("<strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.");
    $info[] = $this->t('Put your site into <a href="@url">maintenance mode</a>.', array(
214
      '@url' => Url::fromRoute('system.site_maintenance_mode')->toString(TRUE)->getGeneratedUrl(),
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
    ));
    $info[] = $this->t('<strong>Back up your database</strong>. This process will change your database values and in case of emergency you may need to revert to a backup.');
    $info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.');
    $build['info'] = array(
      '#theme' => 'item_list',
      '#list_type' => 'ol',
      '#items' => $info,
    );
    $build['info_footer'] = array(
      '#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
    );

    $build['link'] = array(
      '#type' => 'link',
      '#title' => $this->t('Continue'),
      '#attributes' => array('class' => array('button', 'button--primary')),
231 232
      // @todo Revisit once https://www.drupal.org/node/2548095 is in.
      '#url' => Url::fromUri('base://selection'),
233
    );
234 235 236 237 238 239
    return $build;
  }

  /**
   * Renders a list of available database updates.
   *
240 241 242
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
243 244 245
   * @return array
   *   A render array.
   */
246
  protected function selection(Request $request) {
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    // Make sure there is no stale theme registry.
    $this->cache->deleteAll();

    $count = 0;
    $incompatible_count = 0;
    $build['start'] = array(
      '#tree' => TRUE,
      '#type' => 'details',
    );

    // Ensure system.module's updates appear first.
    $build['start']['system'] = array();

    $updates = update_get_update_list();
    $starting_updates = array();
    $incompatible_updates_exist = FALSE;
    foreach ($updates as $module => $update) {
      if (!isset($update['start'])) {
        $build['start'][$module] = array(
          '#type' => 'item',
          '#title' => $module . ' module',
          '#markup'  => $update['warning'],
          '#prefix' => '<div class="messages messages--warning">',
          '#suffix' => '</div>',
        );
        $incompatible_updates_exist = TRUE;
        continue;
      }
      if (!empty($update['pending'])) {
        $starting_updates[$module] = $update['start'];
        $build['start'][$module] = array(
          '#type' => 'hidden',
          '#value' => $update['start'],
        );
        $build['start'][$module . '_updates'] = array(
          '#theme' => 'item_list',
          '#items' => $update['pending'],
          '#title' => $module . ' module',
        );
      }
      if (isset($update['pending'])) {
        $count = $count + count($update['pending']);
      }
    }

    // Find and label any incompatible updates.
    foreach (update_resolve_dependencies($starting_updates) as $data) {
      if (!$data['allowed']) {
        $incompatible_updates_exist = TRUE;
        $incompatible_count++;
        $module_update_key = $data['module'] . '_updates';
        if (isset($build['start'][$module_update_key]['#items'][$data['number']])) {
          if ($data['missing_dependencies']) {
            $text = $this->t('This update will been skipped due to the following missing dependencies:') . '<em>' . implode(', ', $data['missing_dependencies']) . '</em>';
          }
          else {
            $text =  $this->t("This update will be skipped due to an error in the module's code.");
          }
          $build['start'][$module_update_key]['#items'][$data['number']] .= '<div class="warning">' . $text . '</div>';
        }
        // Move the module containing this update to the top of the list.
        $build['start'] = array($module_update_key => $build['start'][$module_update_key]) + $build['start'];
      }
    }

    // Warn the user if any updates were incompatible.
    if ($incompatible_updates_exist) {
      drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning');
    }

    if (empty($count)) {
      drupal_set_message($this->t('No pending updates.'));
      unset($build);
      $build['links'] = array(
        '#theme' => 'links',
322
        '#links' => $this->helpfulLinks($request),
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
      );

      // No updates to run, so caches won't get flushed later.  Clear them now.
      drupal_flush_all_caches();
    }
    else {
      $build['help'] = array(
        '#markup' => '<p>' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '</p>',
        '#weight' => -5,
      );
      if ($incompatible_count) {
        $build['start']['#title'] = $this->formatPlural(
          $count,
          '1 pending update (@number_applied to be applied, @number_incompatible skipped)',
          '@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
          array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count)
        );
      }
      else {
        $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
      }
344 345 346
      // @todo Simplify with https://www.drupal.org/node/2548095
      $base_url = str_replace('/update.php', '', $request->getBaseUrl());
      $url = (new Url('system.db_update', array('op' => 'run')))->setOption('base_url', $base_url);
347 348 349 350 351
      $build['link'] = array(
        '#type' => 'link',
        '#title' => $this->t('Apply pending updates'),
        '#attributes' => array('class' => array('button', 'button--primary')),
        '#weight' => 5,
352 353
        '#url' => $url,
      );
354 355 356 357 358 359 360 361
    }

    return $build;
  }

  /**
   * Displays results of the update script with any accompanying errors.
   *
362 363 364
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
365 366 367
   * @return array
   *   A render array.
   */
368 369 370 371
  protected function results(Request $request) {
    // @todo Simplify with https://www.drupal.org/node/2548095
    $base_url = str_replace('/update.php', '', $request->getBaseUrl());

372 373 374 375
    // Report end result.
    $dblog_exists = $this->moduleHandler->moduleExists('dblog');
    if ($dblog_exists && $this->account->hasPermission('access site reports')) {
      $log_message = $this->t('All errors have been <a href="@url">logged</a>.', array(
376
        '@url' => Url::fromRoute('dblog.overview')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl(),
377 378 379 380 381 382 383
      ));
    }
    else {
      $log_message = $this->t('All errors have been logged.');
    }

    if (!empty($_SESSION['update_success'])) {
384
      $message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => Url::fromRoute('<front>')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl())) . ' ' . $log_message . '</p>';
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
    }
    else {
      $last = reset($_SESSION['updates_remaining']);
      list($module, $version) = array_pop($last);
      $message = '<p class="error">' . $this->t('The update process was aborted prematurely while running <strong>update #@version in @module.module</strong>.', array(
        '@version' => $version,
        '@module' => $module,
      )) . ' ' . $log_message;
      if ($dblog_exists) {
        $message .= ' ' . $this->t('You may need to check the <code>watchdog</code> database table manually.');
      }
      $message .= '</p>';
    }

    if (Settings::get('update_free_access')) {
      $message .= '<p>' . $this->t("<strong>Reminder: don't forget to set the <code>\$settings['update_free_access']</code> value in your <code>settings.php</code> file back to <code>FALSE</code>.</strong>")  . '</p>';
    }

    $build['message'] = array(
      '#markup' => $message,
    );
    $build['links'] = array(
      '#theme' => 'links',
408
      '#links' => $this->helpfulLinks($request),
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
    );

    // Output a list of info messages.
    if (!empty($_SESSION['update_results'])) {
      $all_messages = array();
      foreach ($_SESSION['update_results'] as $module => $updates) {
        if ($module != '#abort') {
          $module_has_message = FALSE;
          $info_messages = array();
          foreach ($updates as $number => $queries) {
            $messages = array();
            foreach ($queries as $query) {
              // If there is no message for this update, don't show anything.
              if (empty($query['query'])) {
                continue;
              }

              if ($query['success']) {
                $messages[] = array(
                  '#wrapper_attributes' => array('class' => array('success')),
                  '#markup' => $query['query'],
                );
              }
              else {
                $messages[] = array(
                  '#wrapper_attributes' => array('class' => array('failure')),
                  '#markup' => '<strong>' . $this->t('Failed:') . '</strong> ' . $query['query'],
                );
              }
            }

            if ($messages) {
              $module_has_message = TRUE;
              $info_messages[] = array(
                '#theme' => 'item_list',
                '#items' => $messages,
                '#title' => $this->t('Update #@count', array('@count' => $number)),
              );
            }
          }

          // If there were any messages then prefix them with the module name
          // and add it to the global message list.
          if ($module_has_message) {
            $all_messages[] = array(
              '#type' => 'container',
              '#prefix' => '<h3>' . $this->t('@module module', array('@module' => $module)) . '</h3>',
              '#children' => $info_messages,
            );
          }
        }
      }
      if ($all_messages) {
462
        $build['query_messages'] = array(
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
          '#type' => 'container',
          '#children' => $all_messages,
          '#attributes' => array('class' => array('update-results')),
          '#prefix' => '<h2>' . $this->t('The following updates returned messages:') . '</h2>',
        );
      }
    }
    unset($_SESSION['update_results']);
    unset($_SESSION['update_success']);
    unset($_SESSION['update_ignore_warnings']);

    return $build;
  }

  /**
   * Renders a list of requirement errors or warnings.
   *
480 481 482
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
483 484 485
   * @return array
   *   A render array.
   */
486
  public function requirements($severity, array $requirements, Request $request) {
487
    $options = $severity == REQUIREMENT_WARNING ? array('continue' => 1) : array();
488 489 490
    // @todo Revisit once https://www.drupal.org/node/2548095 is in. Something
    // like Url::fromRoute('system.db_update')->setOptions() should then be
    // possible.
491
    $try_again_url = Url::fromUri($request->getUriForPath(''))->setOptions(['query' => $options])->toString(TRUE)->getGeneratedUrl();
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523

    $build['status_report'] = array(
      '#theme' => 'status_report',
      '#requirements' => $requirements,
      '#suffix' => $this->t('Check the messages and <a href="@url">try again</a>.', array('@url' => $try_again_url))
    );

    $build['#title'] = $this->t('Requirements problem');
    return $build;
  }

  /**
   * Provides the update task list render array.
   *
   * @param string $active
   *   The active task.
   *   Can be one of 'requirements', 'info', 'selection', 'run', 'results'.
   *
   * @return array
   *   A render array.
   */
  protected function updateTasksList($active = NULL) {
    // Default list of tasks.
    $tasks = array(
      'requirements' => $this->t('Verify requirements'),
      'info' => $this->t('Overview'),
      'selection' => $this->t('Review updates'),
      'run' => $this->t('Run updates'),
      'results' => $this->t('Review log'),
    );

    $task_list = array(
524
      '#theme' => 'maintenance_task_list',
525 526 527 528 529 530 531 532 533 534 535 536 537
      '#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) {
538 539 540 541 542 543 544
    $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)) {
545 546 547
      $this->state->set('system.maintenance_mode', TRUE);
    }

548 549
    $operations = array();

550 551
    // Resolve any update dependencies to determine the actual updates that will
    // be run and the order they will be run in.
552
    $start = $this->getModuleUpdates();
553 554 555 556 557 558 559 560 561 562 563
    $updates = update_resolve_dependencies($start);

    // Store the dependencies for each update function in an array which the
    // batch API can pass in to the batch operation each time it is called. (We
    // do not store the entire update dependency array here because it is
    // potentially very large.)
    $dependency_map = array();
    foreach ($updates as $function => $update) {
      $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array();
    }

564
    // Determine updates to be performed.
565
    foreach ($updates as $function => $update) {
566 567 568 569 570 571 572 573 574 575 576
      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[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function]));
      }
    }
577

578 579 580 581 582 583 584 585 586
    $batch['operations'] = $operations;
    $batch += array(
      'title' => $this->t('Updating'),
      'init_message' => $this->t('Starting updates'),
      'error_message' => $this->t('An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.'),
      'finished' => array('\Drupal\system\Controller\DbUpdateController', 'batchFinished'),
    );
    batch_set($batch);

587 588
    // @todo Revisit once https://www.drupal.org/node/2548095 is in.
    return batch_process(Url::fromUri('base://results'), Url::fromUri('base://start'));
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
  }

  /**
   * 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
615 616
    // previously not in maintenance mode.
    if (empty($_SESSION['maintenance_mode'])) {
617 618
      \Drupal::state()->set('system.maintenance_mode', FALSE);
    }
619
    unset($_SESSION['maintenance_mode']);
620 621 622 623 624
  }

  /**
   * Provides links to the homepage and administration pages.
   *
625 626 627
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
628 629 630
   * @return array
   *   An array of links.
   */
631 632 633
  protected function helpfulLinks(Request $request) {
    // @todo Simplify with https://www.drupal.org/node/2548095
    $base_url = str_replace('/update.php', '', $request->getBaseUrl());
634 635
    $links['front'] = array(
      'title' => $this->t('Front page'),
636
      'url' => Url::fromRoute('<front>')->setOption('base_url', $base_url),
637 638 639 640
    );
    if ($this->account->hasPermission('access administration pages')) {
      $links['admin-pages'] = array(
        'title' => $this->t('Administration pages'),
641
        'url' => Url::fromRoute('system.admin')->setOption('base_url', $base_url),
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
      );
    }
    return $links;
  }

  /**
   * Retrieves module updates.
   *
   * @return array
   *   The module updates that can be performed.
   */
  protected function getModuleUpdates() {
    $return = array();
    $updates = update_get_update_list();
    foreach ($updates as $module => $update) {
      $return[$module] = $update['start'];
    }

    return $return;
  }

}