Version 1
[yaffs-website] / web / core / modules / system / src / Controller / DbUpdateController.php
diff --git a/web/core/modules/system/src/Controller/DbUpdateController.php b/web/core/modules/system/src/Controller/DbUpdateController.php
new file mode 100644 (file)
index 0000000..9e0a374
--- /dev/null
@@ -0,0 +1,705 @@
+<?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) {
+      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'] = [
+        '#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;
+  }
+
+}