Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
[yaffs-website] / vendor / drush / drush / src / Commands / core / UpdateDBCommands.php
1 <?php
2 namespace Drush\Commands\core;
3
4 use Consolidation\Log\ConsoleLogLevel;
5 use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
6 use Drupal\Core\Logger\RfcLogLevel;
7 use Drupal\Core\Utility\Error;
8 use Drupal\Core\Entity\EntityStorageException;
9 use Drush\Commands\DrushCommands;
10 use Drush\Drush;
11 use Drush\Exceptions\UserAbortException;
12 use Psr\Log\LogLevel;
13 use Symfony\Component\Console\Output\OutputInterface;
14
15 class UpdateDBCommands extends DrushCommands
16 {
17     protected $cache_clear;
18
19     protected $maintenanceModeOriginalState;
20
21     /**
22      * Apply any database updates required (as with running update.php).
23      *
24      * @command updatedb
25      * @option cache-clear Clear caches upon completion.
26      * @option entity-updates Run automatic entity schema updates at the end of any update hooks.
27      * @option post-updates Run post updates after hook_update_n and entity updates.
28      * @bootstrap full
29      * @kernel update
30      * @aliases updb
31      */
32     public function updatedb($options = ['cache-clear' => true, 'entity-updates' => false, 'post-updates' => true])
33     {
34         $this->cache_clear = $options['cache-clear'];
35         require_once DRUPAL_ROOT . '/core/includes/install.inc';
36         require_once DRUPAL_ROOT . '/core/includes/update.inc';
37         drupal_load_updates();
38
39         // Disables extensions that have a lower Drupal core major version, or too high of a PHP requirement.
40         // Those are rare, and this function does a full rebuild. So commenting it out for now.
41         // update_fix_compatibility();
42
43         // Check requirements before updating.
44         if (!$this->updateCheckRequirements()) {
45             if (!$this->io()->confirm(dt('Requirements check reports errors. Do you wish to continue?'))) {
46                 throw new UserAbortException();
47             }
48         }
49
50         $return = drush_invoke_process('@self', 'updatedb:status', [], ['entity-updates' => $options['entity-updates'], 'post-updates' => $options['post-updates']]);
51         if ($return['error_status']) {
52             throw new \Exception('Failed getting update status.');
53         } elseif (empty($return['object'])) {
54             // Do nothing. updatedb:status already logged a message.
55         } else {
56             if (!$this->io()->confirm(dt('Do you wish to run the specified pending updates?'))) {
57                 throw new UserAbortException();
58             }
59             if (Drush::simulate()) {
60                 $success = true;
61             } else {
62                 $success = $this->updateBatch($options);
63                 // Caches were just cleared in updateFinished callback.
64             }
65
66             if (!$success) {
67                 drush_set_context('DRUSH_EXIT_CODE', DRUSH_FRAMEWORK_ERROR);
68             }
69
70             $level = $success ? ConsoleLogLevel::SUCCESS : LogLevel::ERROR;
71             $this->logger()->log($level, dt('Finished performing updates.'));
72         }
73     }
74
75     /**
76      * Apply pending entity schema updates.
77      *
78      * @command entity:updates
79      * @option cache-clear Set to 0 to suppress normal cache clearing; the caller should then clear if needed.
80      * @bootstrap full
81      * @kernel update
82      * @aliases entup,entity-updates
83      * @usage drush updatedb:status --entity-updates | grep entity-update
84      *   Use updatedb:status to detect pending updates.
85      *
86      */
87     public function entityUpdates($options = ['cache-clear' => true])
88     {
89         if (Drush::simulate()) {
90             throw new \Exception(dt('entity-updates command does not support --simulate option.'));
91         }
92
93         if ($this->entityUpdatesMain() === false) {
94             throw new \Exception('Entity updates not run.');
95         }
96
97         if ($options['cache-clear']) {
98             drush_drupal_cache_clear_all();
99         }
100
101         $this->logger()->success(dt('Finished performing updates.'));
102     }
103
104     /**
105      * List any pending database updates.
106      *
107      * @command updatedb:status
108      * @option entity-updates Show entity schema updates.
109      * @option post-updates Show post updates.
110      * @bootstrap full
111      * @kernel update
112      * @aliases updbst,updatedb-status
113      * @field-labels
114      *   module: Module
115      *   update_id: Update ID
116      *   description: Description
117      *   type: Type
118      * @default-fields module,update_id,type,description
119      * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
120      */
121     public function updatedbStatus($options = ['format'=> 'table', 'entity-updates' => true, 'post-updates' => true])
122     {
123         require_once DRUSH_DRUPAL_CORE . '/includes/install.inc';
124         drupal_load_updates();
125         list($pending, $start) = $this->getUpdatedbStatus($options);
126         if (empty($pending)) {
127             $this->logger()->success(dt("No database updates required."));
128         } else {
129             return new RowsOfFields($pending);
130         }
131     }
132
133     /**
134      * Process operations in the specified batch set.
135      *
136      * @command updatedb:batch-process
137      * @param string $batch_id The batch id that will be processed.
138      * @bootstrap full
139      * @kernel update
140      * @hidden
141      */
142     public function process($batch_id)
143     {
144         // Suppress the output of the batch process command. This is intended to
145         // be passed to the initiating command rather than being output to the
146         // console.
147         $this->output()->setVerbosity(OutputInterface::VERBOSITY_QUIET);
148         return drush_batch_command($batch_id);
149     }
150
151     /**
152      * Perform one update and store the results which will later be displayed on
153      * the finished page.
154      *
155      * An update function can force the current and all later updates for this
156      * module to abort by returning a $ret array with an element like:
157      * $ret['#abort'] = array('success' => FALSE, 'query' => 'What went wrong');
158      * The schema version will not be updated in this case, and all the
159      * aborted updates will continue to appear on update.php as updates that
160      * have not yet been run.
161      *
162      * @param $module
163      *   The module whose update will be run.
164      * @param $number
165      *   The update number to run.
166      * @param $context
167      *   The batch context array
168      */
169     public function updateDoOne($module, $number, $dependency_map, &$context)
170     {
171         $function = $module . '_update_' . $number;
172
173         // Disable config entity overrides.
174         if (!defined('MAINTENANCE_MODE')) {
175             define('MAINTENANCE_MODE', 'update');
176         }
177
178         // If this update was aborted in a previous step, or has a dependency that
179         // was aborted in a previous step, go no further.
180         if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, [$function]))) {
181             return;
182         }
183
184         $context['log'] = false;
185
186         \Drupal::moduleHandler()->loadInclude($module, 'install');
187
188         $ret = [];
189         if (function_exists($function)) {
190             try {
191                 if ($context['log']) {
192                     Database::startLog($function);
193                 }
194
195                 $this->logger()->notice("Update started: $function");
196                 $ret['results']['query'] = $function($context['sandbox']);
197                 $ret['results']['success'] = true;
198             } catch (\Throwable $e) {
199                 // PHP 7 introduces Throwable, which covers both Error and Exception throwables.
200                 $ret['#abort'] = ['success' => false, 'query' => $e->getMessage()];
201                 $this->logger()->error($e->getMessage());
202             } catch (\Exception $e) {
203                 // In order to be compatible with PHP 5 we also catch regular Exceptions.
204                 $ret['#abort'] = ['success' => false, 'query' => $e->getMessage()];
205                 $this->logger()->error($e->getMessage());
206             }
207
208             if ($context['log']) {
209                 $ret['queries'] = Database::getLog($function);
210             }
211         } else {
212             $ret['#abort'] = ['success' => false];
213             $this->logger()->warning(dt('Update function @function not found', ['@function' => $function]));
214         }
215
216         if (isset($context['sandbox']['#finished'])) {
217             $context['finished'] = $context['sandbox']['#finished'];
218             unset($context['sandbox']['#finished']);
219         }
220
221         if (!isset($context['results'][$module])) {
222             $context['results'][$module] = [];
223         }
224         if (!isset($context['results'][$module][$number])) {
225             $context['results'][$module][$number] = [];
226         }
227         $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);
228
229         // Log the message that was returned.
230         if (!empty($ret['results']['query'])) {
231             $this->logger()->notice(strip_tags((string) $ret['results']['query']));
232         }
233
234         if (!empty($ret['#abort'])) {
235             // Record this function in the list of updates that were aborted.
236             $context['results']['#abort'][] = $function;
237             // Setting this value will output an error message.
238             // @see \DrushBatchContext::offsetSet()
239             $context['error_message'] = "Update failed: $function";
240         }
241
242         // Record the schema update if it was completed successfully.
243         if ($context['finished'] == 1 && empty($ret['#abort'])) {
244             drupal_set_installed_schema_version($module, $number);
245             // Setting this value will output a success message.
246             // @see \DrushBatchContext::offsetSet()
247             $context['message'] = "Update completed: $function";
248         }
249     }
250
251     /**
252      * Batch command that executes a single post-update.
253      *
254      * @param string $function
255      *   The post-update function to execute.
256      * @param array $context
257      *   The batch context.
258      */
259     public function updateDoOnePostUpdate($function, &$context)
260     {
261         $ret = [];
262
263         // Disable config entity overrides.
264         if (!defined('MAINTENANCE_MODE')) {
265             define('MAINTENANCE_MODE', 'update');
266         }
267
268         // If this update was aborted in a previous step, or has a dependency that was
269         // aborted in a previous step, go no further.
270         if (!empty($context['results']['#abort'])) {
271             return;
272         }
273
274         list($module, $name) = explode('_post_update_', $function, 2);
275         module_load_include('php', $module, $module . '.post_update');
276         if (function_exists($function)) {
277             $this->logger()->notice("Update started: $function");
278             try {
279                 $ret['results']['query'] = $function($context['sandbox']);
280                 $ret['results']['success'] = true;
281
282                 if (!isset($context['sandbox']['#finished']) || (isset($context['sandbox']['#finished']) && $context['sandbox']['#finished'] >= 1)) {
283                     \Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]);
284                 }
285             } catch (\Exception $e) {
286                 // @TODO We may want to do different error handling for different exception
287                 // types, but for now we'll just log the exception and return the message
288                 // for printing.
289                 // @see https://www.drupal.org/node/2564311
290                 $this->logger()->error($e->getMessage());
291
292                 $variables = Error::decodeException($e);
293                 unset($variables['backtrace']);
294                 $ret['#abort'] = [
295                     'success' => false,
296                     'query' => t('%type: @message in %function (line %line of %file).', $variables),
297                 ];
298             }
299         }
300
301         if (isset($context['sandbox']['#finished'])) {
302             $context['finished'] = $context['sandbox']['#finished'];
303             unset($context['sandbox']['#finished']);
304         }
305         if (!isset($context['results'][$module][$name])) {
306             $context['results'][$module][$name] = [];
307         }
308         $context['results'][$module][$name] = array_merge($context['results'][$module][$name], $ret);
309
310         // Log the message that was returned.
311         if (!empty($ret['results']['query'])) {
312             $this->logger()->notice(strip_tags((string) $ret['results']['query']));
313         }
314
315         if (!empty($ret['#abort'])) {
316             // Record this function in the list of updates that were aborted.
317             $context['results']['#abort'][] = $function;
318             // Setting this value will output an error message.
319             // @see \DrushBatchContext::offsetSet()
320             $context['error_message'] = "Update failed: $function";
321         } else {
322             // Setting this value will output a success message.
323             // @see \DrushBatchContext::offsetSet()
324             $context['message'] = "Update completed: $function";
325         }
326     }
327
328     /**
329      * Start the database update batch process.
330      */
331     public function updateBatch($options)
332     {
333         $start = $this->getUpdateList();
334         // Resolve any update dependencies to determine the actual updates that will
335         // be run and the order they will be run in.
336         $updates = update_resolve_dependencies($start);
337
338         // Store the dependencies for each update function in an array which the
339         // batch API can pass in to the batch operation each time it is called. (We
340         // do not store the entire update dependency array here because it is
341         // potentially very large.)
342         $dependency_map = [];
343         foreach ($updates as $function => $update) {
344             $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
345         }
346
347         $operations = [];
348
349         foreach ($updates as $update) {
350             if ($update['allowed']) {
351                 // Set the installed version of each module so updates will start at the
352                 // correct place. (The updates are already sorted, so we can simply base
353                 // this on the first one we come across in the above foreach loop.)
354                 if (isset($start[$update['module']])) {
355                     drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
356                     unset($start[$update['module']]);
357                 }
358                 // Add this update function to the batch.
359                 $function = $update['module'] . '_update_' . $update['number'];
360                 $operations[] = [[$this, 'updateDoOne'], [$update['module'], $update['number'], $dependency_map[$function]]];
361             }
362         }
363
364         // Perform entity definition updates, which will update storage
365         // schema if needed. If module update functions need to work with specific
366         // entity schema they should call the entity update service for the specific
367         // update themselves.
368         // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyEntityUpdate()
369         // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyFieldUpdate()
370         if ($options['entity-updates'] && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) {
371             $operations[] = [[$this, 'updateEntityDefinitions'], []];
372         }
373
374         // Lastly, apply post update hooks if specified.
375         if ($options['post-updates']) {
376             $post_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateFunctions();
377             if ($post_updates) {
378                 if ($operations) {
379                     // Only needed if we performed updates earlier.
380                     $operations[] = [[$this, 'cacheRebuild'], []];
381                 }
382                 foreach ($post_updates as $function) {
383                     $operations[] = [[$this, 'updateDoOnePostUpdate'], [$function]];
384                 }
385             }
386         }
387
388         $batch['operations'] = $operations;
389         $batch += [
390             'title' => 'Updating',
391             'init_message' => 'Starting updates',
392             'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.',
393             'finished' => [$this, 'updateFinished'],
394             'file' => 'core/includes/update.inc',
395         ];
396         batch_set($batch);
397
398         // See updateFinished() for the restore of maint mode.
399         $this->maintenanceModeOriginalState = \Drupal::service('state')->get('system.maintenance_mode');
400         \Drupal::service('state')->set('system.maintenance_mode', true);
401         $result = drush_backend_batch_process('updatedb:batch-process');
402
403         $success = false;
404         if (!is_array($result)) {
405             $this->logger()->error(dt('Batch process did not return a result array. Returned: !type', ['!type' => gettype($result)]));
406         } elseif (!array_key_exists('object', $result)) {
407             $this->logger()->error(dt('Batch process did not return a result object.'));
408         } elseif (!empty($result['object'][0]['#abort'])) {
409             // Whenever an error occurs the batch process does not continue, so
410             // this array should only contain a single item, but we still output
411             // all available data for completeness.
412             $this->logger()->error(dt('Update aborted by: !process', [
413                 '!process' => implode(', ', $result['object'][0]['#abort']),
414             ]));
415         } else {
416             $success = true;
417         }
418
419         return $success;
420     }
421
422     /**
423      * Apply entity schema updates.
424      */
425     public function updateEntityDefinitions(&$context)
426     {
427         try {
428             \Drupal::entityDefinitionUpdateManager()->applyupdates();
429         } catch (EntityStorageException $e) {
430             watchdog_exception('update', $e);
431             $variables = Error::decodeException($e);
432             unset($variables['backtrace']);
433             // The exception message is run through
434             // \Drupal\Component\Utility\SafeMarkup::checkPlain() by
435             // \Drupal\Core\Utility\Error::decodeException().
436             $ret['#abort'] = ['success' => false, 'query' => t('%type: !message in %function (line %line of %file).', $variables)];
437             $context['results']['core']['update_entity_definitions'] = $ret;
438             $context['results']['#abort'][] = 'update_entity_definitions';
439         }
440     }
441
442     // Copy of protected \Drupal\system\Controller\DbUpdateController::getModuleUpdates.
443     public function getUpdateList()
444     {
445         $return = [];
446         $updates = update_get_update_list();
447         foreach ($updates as $module => $update) {
448             $return[$module] = $update['start'];
449         }
450
451         return $return;
452     }
453
454     /**
455      * Clears caches and rebuilds the container.
456      *
457      * This is called in between regular updates and post updates. Do not use
458      * drush_drupal_cache_clear_all() as the cache clearing and container rebuild
459      * must happen in the same process that the updates are run in.
460      *
461      * Drupal core's update.php uses drupal_flush_all_caches() directly without
462      * explicitly rebuilding the container as the container is rebuilt on the next
463      * HTTP request of the batch.
464      *
465      * @see drush_drupal_cache_clear_all()
466      * @see \Drupal\system\Controller\DbUpdateController::triggerBatch()
467      */
468     public function cacheRebuild()
469     {
470         drupal_flush_all_caches();
471         \Drupal::service('kernel')->rebuildContainer();
472         // Load the module data which has been removed when the container was
473         // rebuilt.
474         $module_handler = \Drupal::moduleHandler();
475         $module_handler->loadAll();
476         $module_handler->invokeAll('rebuild');
477     }
478
479     /**
480      * Batch update callback, clears the cache if needed, and restores maint mode.
481      *
482      * @see \Drupal\system\Controller\DbUpdateController::batchFinished()
483      * @see \Drupal\system\Controller\DbUpdateController::results()
484      *
485      * @param boolean $success Whether the batch ended without a fatal error.
486      * @param array $results
487      * @param array $operations
488      */
489     public function updateFinished($success, $results, $operations)
490     {
491         if (!$this->cache_clear) {
492             $this->logger()->info(dt("Skipping cache-clear operation due to --no-cache-clear option."));
493         } else {
494             drupal_flush_all_caches();
495         }
496
497         \Drupal::service('state')->set('system.maintenance_mode', $this->maintenanceModeOriginalState);
498     }
499
500     /**
501      * Return a 2 item array with
502      *  - an array where each item is a 4 item associative array describing a pending update.
503      *  - an array listing the first update to run, keyed by module.
504      */
505     public function getUpdatedbStatus(array $options)
506     {
507         require_once DRUPAL_ROOT . '/core/includes/update.inc';
508         $pending = \update_get_update_list();
509
510         $return = [];
511         // Ensure system module's updates run first.
512         $start['system'] = [];
513
514         foreach ($pending as $module => $updates) {
515             if (isset($updates['start'])) {
516                 foreach ($updates['pending'] as $update_id => $description) {
517                     // Strip cruft from front.
518                     $description = str_replace($update_id . ' -   ', '', $description);
519                     $return[$module . "_update_$update_id"] = [
520                         'module' => $module,
521                         'update_id' => $update_id,
522                         'description' => $description,
523                         'type'=> 'hook_update_n'
524                     ];
525                 }
526                 if (isset($updates['start'])) {
527                     $start[$module] = $updates['start'];
528                 }
529             }
530         }
531
532         // Append row(s) for pending entity definition updates.
533         if ($options['entity-updates']) {
534             foreach (\Drupal::entityDefinitionUpdateManager()
535                          ->getChangeSummary() as $entity_type_id => $changes) {
536                 foreach ($changes as $change) {
537                     $return[] = [
538                         'module' => dt('@type entity type', ['@type' => $entity_type_id]),
539                         'update_id' => '',
540                         'description' => strip_tags($change),
541                         'type' => 'entity-update'
542                     ];
543                 }
544             }
545         }
546
547         // Pending hook_post_update_X() implementations.
548         $post_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateInformation();
549         if ($options['post-updates']) {
550             foreach ($post_updates as $module => $post_update) {
551                 foreach ($post_update as $key => $list) {
552                     if ($key == 'pending') {
553                         foreach ($list as $id => $item) {
554                             $return[$module . '-post-' . $id] = [
555                                 'module' => $module,
556                                 'update_id' => $id,
557                                 'description' => $item,
558                                 'type' => 'post-update'
559                             ];
560                         }
561                     }
562                 }
563             }
564         }
565
566         return [$return, $start];
567     }
568
569     /**
570      * Apply pending entity schema updates.
571      */
572     public function entityUpdatesMain()
573     {
574         $change_summary = \Drupal::entityDefinitionUpdateManager()->getChangeSummary();
575         if (!empty($change_summary)) {
576             $this->output()->writeln(dt('The following updates are pending:'));
577             $this->io()->newLine();
578
579             foreach ($change_summary as $entity_type_id => $changes) {
580                 $this->output()->writeln($entity_type_id . ' entity type : ');
581                 foreach ($changes as $change) {
582                     $this->output()->writeln(strip_tags($change), 2);
583                 }
584             }
585
586             if (!$this->io()->confirm(dt('Do you wish to run all pending updates?'))) {
587                 throw new UserAbortException();
588             }
589
590             $operations[] = [[$this, 'updateEntityDefinitions'], []];
591
592
593             $batch['operations'] = $operations;
594             $batch += [
595                 'title' => 'Updating',
596                 'init_message' => 'Starting updates',
597                 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.',
598                 'finished' => [$this, 'updateFinished'],
599             ];
600             batch_set($batch);
601
602             // See updateFinished() for the restore of maint mode.
603             $this->maintenanceModeOriginalState = \Drupal::service('state')->get('system.maintenance_mode');
604             \Drupal::service('state')->set('system.maintenance_mode', true);
605             drush_backend_batch_process();
606         } else {
607             $this->logger()->success(dt("No entity schema updates required"));
608         }
609     }
610
611     /**
612      * Log messages for any requirements warnings/errors.
613      */
614     public function updateCheckRequirements()
615     {
616         $return = true;
617
618         \Drupal::moduleHandler()->resetImplementations();
619         $requirements = update_check_requirements();
620         $severity = drupal_requirements_severity($requirements);
621
622         // If there are issues, report them.
623         if ($severity != REQUIREMENT_OK) {
624             if ($severity === REQUIREMENT_ERROR) {
625                 $return = false;
626             }
627             foreach ($requirements as $requirement) {
628                 if (isset($requirement['severity']) && $requirement['severity'] != REQUIREMENT_OK) {
629                     $message = isset($requirement['description']) ? $requirement['description'] : '';
630                     if (isset($requirement['value']) && $requirement['value']) {
631                         $message .= ' (Currently using '. $requirement['title'] .' '. $requirement['value'] .')';
632                     }
633                     $log_level = $requirement['severity'] === REQUIREMENT_ERROR ? LogLevel::ERROR : LogLevel::WARNING;
634                     $this->logger()->log($log_level, $message);
635                 }
636             }
637         }
638
639         return $return;
640     }
641 }