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