872c664f3294038dd784c17491dfa79de51e6979
[yaffs-website] / vendor / drush / drush / src / Drupal / Commands / config / ConfigCommands.php
1 <?php
2 namespace Drush\Drupal\Commands\config;
3
4 use Consolidation\AnnotatedCommand\CommandError;
5 use Consolidation\AnnotatedCommand\CommandData;
6 use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\Config\FileStorage;
9 use Drupal\Core\Config\StorageComparer;
10 use Drupal\Core\Config\StorageInterface;
11 use Drush\Commands\DrushCommands;
12 use Drush\Drush;
13 use Drush\Utils\FsUtils;
14 use Symfony\Component\Console\Helper\Table;
15 use Symfony\Component\Console\Input\InputInterface;
16 use Symfony\Component\Console\Output\ConsoleOutputInterface;
17 use Symfony\Component\Console\Output\OutputInterface;
18 use Symfony\Component\Yaml\Parser;
19 use Webmozart\PathUtil\Path;
20
21 class ConfigCommands extends DrushCommands
22 {
23
24     /**
25      * @var ConfigFactoryInterface
26      */
27     protected $configFactory;
28
29     /**
30      * @return ConfigFactoryInterface
31      */
32     public function getConfigFactory()
33     {
34         return $this->configFactory;
35     }
36
37
38     /**
39      * ConfigCommands constructor.
40      * @param ConfigFactoryInterface $configFactory
41      */
42     public function __construct($configFactory)
43     {
44         parent::__construct();
45         $this->configFactory = $configFactory;
46     }
47
48     /**
49      * Display a config value, or a whole configuration object.
50      *
51      * @command config:get
52      * @validate-config-name
53      * @interact-config-name
54      * @param $config_name The config object name, for example "system.site".
55      * @param $key The config key, for example "page.front". Optional.
56      * @option source The config storage source to read. Additional labels may be defined in settings.php.
57      * @option include-overridden Apply module and settings.php overrides to values.
58      * @usage drush config:get system.site
59      *   Displays the system.site config.
60      * @usage drush config:get system.site page.front
61      *   Gets system.site:page.front value.
62      * @aliases cget,config-get
63      */
64     public function get($config_name, $key = '', $options = ['format' => 'yaml', 'source' => 'active', 'include-overridden' => false])
65     {
66         // Displaying overrides only applies to active storage.
67         $factory = $this->getConfigFactory();
68         $config = $options['include-overridden'] ? $factory->get($config_name) : $factory->getEditable($config_name);
69         $value = $config->get($key);
70         // @todo If the value is TRUE (for example), nothing gets printed. Is this yaml formatter's fault?
71         return $key ? ["$config_name:$key" => $value] : $value;
72     }
73
74     /**
75      * Set config value directly. Does not perform a config import.
76      *
77      * @command config:set
78      * @validate-config-name
79      * @todo @interact-config-name deferred until we have interaction for key.
80      * @param $config_name The config object name, for example "system.site".
81      * @param $key The config key, for example "page.front".
82      * @param $value The value to assign to the config key. Use '-' to read from STDIN.
83      * @option format Format to parse the object. Use "string" for string (default), and "yaml" for YAML.
84      * // A convenient way to pass a multiline value within a backend request.
85      * @option value The value to assign to the config key (if any).
86      * @hidden-options value
87      * @usage drush config:set system.site page.front node
88      *   Sets system.site:page.front to "node".
89      * @aliases cset,config-set
90      */
91     public function set($config_name, $key, $value = null, $options = ['format' => 'string', 'value' => self::REQ])
92     {
93         // This hidden option is a convenient way to pass a value without passing a key.
94         $data = $options['value'] ?: $value;
95
96         if (!isset($data)) {
97             throw new \Exception(dt('No config value specified.'));
98         }
99
100         $config = $this->getConfigFactory()->getEditable($config_name);
101         // Check to see if config key already exists.
102         $new_key = $config->get($key) === null;
103
104         // Special flag indicating that the value has been passed via STDIN.
105         if ($data === '-') {
106             $data = stream_get_contents(STDIN);
107         }
108
109         // Now, we parse the value.
110         switch ($options['format']) {
111             case 'yaml':
112                 $parser = new Parser();
113                 $data = $parser->parse($data, true);
114         }
115
116         if (is_array($data) && $this->io()->confirm(dt('Do you want to update or set multiple keys on !name config.', ['!name' => $config_name]))) {
117             foreach ($data as $key => $value) {
118                 $config->set($key, $value);
119             }
120             return $config->save();
121         } else {
122             $confirmed = false;
123             if ($config->isNew() && $this->io()->confirm(dt('!name config does not exist. Do you want to create a new config object?', ['!name' => $config_name]))) {
124                 $confirmed = true;
125             } elseif ($new_key && $this->io()->confirm(dt('!key key does not exist in !name config. Do you want to create a new config key?', ['!key' => $key, '!name' => $config_name]))) {
126                 $confirmed = true;
127             } elseif ($this->io()->confirm(dt('Do you want to update !key key in !name config?', ['!key' => $key, '!name' => $config_name]))) {
128                 $confirmed = true;
129             }
130             if ($confirmed && !\Drush\Drush::simulate()) {
131                 return $config->set($key, $data)->save();
132             }
133         }
134     }
135
136     /**
137      * Open a config file in a text editor. Edits are imported after closing editor.
138      *
139      * @command config:edit
140      * @validate-config-name
141      * @interact-config-name
142      * @param $config_name The config object name, for example "system.site".
143      * @optionset_get_editor
144      * @allow_additional_options config-import
145      * @hidden-options source,partial
146      * @usage drush config:edit image.style.large
147      *   Edit the image style configurations.
148      * @usage drush config:edit
149      *   Choose a config file to edit.
150      * @usage drush --bg config-edit image.style.large
151      *   Return to shell prompt as soon as the editor window opens.
152      * @aliases cedit,config-edit
153      * @validate-module-enabled config
154      */
155     public function edit($config_name)
156     {
157         $config = $this->getConfigFactory()->get($config_name);
158         $active_storage = $config->getStorage();
159         $contents = $active_storage->read($config_name);
160
161         // Write tmp YAML file for editing
162         $temp_dir = drush_tempdir();
163         $temp_storage = new FileStorage($temp_dir);
164         $temp_storage->write($config_name, $contents);
165
166         $exec = drush_get_editor();
167         drush_shell_exec_interactive($exec, $temp_storage->getFilePath($config_name));
168
169         // Perform import operation if user did not immediately exit editor.
170         if (!$options['bg']) {
171             $options = Drush::redispatchOptions()   + ['partial' => true, 'source' => $temp_dir];
172             $backend_options = ['interactive' => true];
173             return (bool) drush_invoke_process('@self', 'config-import', [], $options, $backend_options);
174         }
175     }
176
177     /**
178      * Delete a configuration key, or a whole object.
179      *
180      * @command config:delete
181      * @validate-config-name
182      * @interact-config-name
183      * @param $config_name The config object name, for example "system.site".
184      * @param $key A config key to clear, for example "page.front".
185      * @usage drush config:delete system.site
186      *   Delete the the system.site config object.
187      * @usage drush config:delete system.site page.front node
188      *   Delete the 'page.front' key from the system.site object.
189      * @aliases cdel,config-delete
190      */
191     public function delete($config_name, $key = null)
192     {
193         $config = $this->getConfigFactory()->getEditable($config_name);
194         if ($key) {
195             if ($config->get($key) === null) {
196                 throw new \Exception(dt('Configuration key !key not found.', ['!key' => $key]));
197             }
198             $config->clear($key)->save();
199         } else {
200             $config->delete();
201         }
202     }
203
204     /**
205      * Display status of configuration (differences between the filesystem configuration and database configuration).
206      *
207      * @command config:status
208      * @option state  A comma-separated list of states to filter results.
209      * @option prefix Prefix The config prefix. For example, "system". No prefix will return all names in the system.
210      * @option string $label A config directory label (i.e. a key in \$config_directories array in settings.php).
211      * @usage drush config:status
212      *   Display configuration items that need to be synchronized.
213      * @usage drush config:status --state=Identical
214      *   Display configuration items that are in default state.
215      * @usage drush config:status --state='Only in sync dir' --prefix=node.type.
216      *   Display all content types that would be created in active storage on configuration import.
217      * @usage drush config:status --state=Any --format=list
218      *   List all config names.
219      * @field-labels
220      *   name: Name
221      *   state: State
222      * @default-fields name,state
223      * @aliases cst,config-status
224      * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
225      */
226     public function status($options = ['state' => 'Only in DB,Only in sync dir,Different', 'prefix' => self::REQ, 'label' => self::REQ])
227     {
228         $config_list = array_fill_keys(
229             $this->configFactory->listAll($options['prefix']),
230             'Identical'
231         );
232
233         $directory = $this->getDirectory($options['label']);
234         $storage = $this->getStorage($directory);
235         $state_map = [
236             'create' => 'Only in DB',
237             'update' => 'Different',
238             'delete' => 'Only in sync dir',
239         ];
240         foreach ($this->getChanges($storage) as $collection) {
241             foreach ($collection as $operation => $configs) {
242                 foreach ($configs as $config) {
243                     if (!$options['prefix'] || strpos($config, $options['prefix']) === 0) {
244                         $config_list[$config] = $state_map[$operation];
245                     }
246                 }
247             }
248         }
249
250         if ($options['state']) {
251             $allowed_states = explode(',', $options['state']);
252             if (!in_array('Any', $allowed_states)) {
253                 $config_list = array_filter($config_list, function ($state) use ($allowed_states) {
254                      return in_array($state, $allowed_states);
255                 });
256             }
257         }
258
259         ksort($config_list);
260
261         $rows = [];
262         $color_map = [
263             'Only in DB' => 'green',
264             'Only in sync dir' => 'red',
265             'Different' => 'yellow',
266             'Identical' => 'white',
267         ];
268
269         foreach ($config_list as $config => $state) {
270             if ($options['format'] == 'table' && $state != 'Identical') {
271                 $state = "<fg={$color_map[$state]};options=bold>$state</>";
272             }
273             $rows[$config] = [
274                 'name' => $config,
275                 'state' => $state,
276             ];
277         }
278
279         if ($rows) {
280             return new RowsOfFields($rows);
281         } else {
282             $this->logger()->notice(dt('No differences between DB and sync directory.'));
283         }
284     }
285
286     /**
287      * Determine which configuration directory to use and return directory path.
288      *
289      * Directory path is determined based on the following precedence:
290      *   1. User-provided $directory.
291      *   2. Directory path corresponding to $label (mapped via $config_directories in settings.php).
292      *   3. Default sync directory
293      *
294      * @param string $label
295      *   A configuration directory label.
296      * @param string $directory
297      *   A configuration directory.
298      */
299     public static function getDirectory($label, $directory = null)
300     {
301         $return = null;
302         // If the user provided a directory, use it.
303         if (!empty($directory)) {
304             if ($directory === true) {
305                 // The user did not pass a specific directory, make one.
306                 $return = FsUtils::prepareBackupDir('config-import-export');
307             } else {
308                 // The user has specified a directory.
309                 drush_mkdir($directory);
310                 $return = $directory;
311             }
312         } else {
313             // If a directory isn't specified, use the label argument or default sync directory.
314             $return = \config_get_config_directory($label ?: CONFIG_SYNC_DIRECTORY);
315         }
316         return Path::canonicalize($return);
317     }
318
319     /**
320      * Returns the difference in configuration between active storage and target storage.
321      */
322     public function getChanges($target_storage)
323     {
324         /** @var StorageInterface $active_storage */
325         $active_storage = \Drupal::service('config.storage');
326
327         $config_comparer = new StorageComparer($active_storage, $target_storage, \Drupal::service('config.manager'));
328
329         $change_list = [];
330         if ($config_comparer->createChangelist()->hasChanges()) {
331             foreach ($config_comparer->getAllCollectionNames() as $collection) {
332                 $change_list[$collection] = $config_comparer->getChangelist(null, $collection);
333             }
334         }
335         return $change_list;
336     }
337
338     /**
339      * Get storage corresponding to a configuration directory.
340      */
341     public function getStorage($directory)
342     {
343         if ($directory == \config_get_config_directory(CONFIG_SYNC_DIRECTORY)) {
344             return \Drupal::service('config.storage.sync');
345         } else {
346             return new FileStorage($directory);
347         }
348     }
349
350     /**
351      * Build a table of config changes.
352      *
353      * @param array $config_changes
354      *   An array of changes keyed by collection.
355      *
356      * @return Table A Symfony table object.
357      */
358     public static function configChangesTable(array $config_changes, OutputInterface $output, $use_color = true)
359     {
360         $rows = [];
361         foreach ($config_changes as $collection => $changes) {
362             foreach ($changes as $change => $configs) {
363                 switch ($change) {
364                     case 'delete':
365                         $colour = '<fg=white;bg=red>';
366                         break;
367                     case 'update':
368                         $colour = '<fg=black;bg=yellow>';
369                         break;
370                     case 'create':
371                         $colour = '<fg=white;bg=green>';
372                         break;
373                     default:
374                         $colour = "<fg=black;bg=cyan>";
375                         break;
376                 }
377                 if ($use_color) {
378                     $prefix = $colour;
379                     $suffix = '</>';
380                 } else {
381                     $prefix = $suffix = '';
382                 }
383                 foreach ($configs as $config) {
384                     $rows[] = [
385                         $collection,
386                         $config,
387                         $prefix . ucfirst($change) . $suffix,
388                     ];
389                 }
390             }
391         }
392         $table = new Table($output);
393         $table->setHeaders(['Collection', 'Config', 'Operation']);
394         $table->addRows($rows);
395         return $table;
396     }
397
398     /**
399      * @hook interact @interact-config-name
400      */
401     public function interactConfigName($input, $output)
402     {
403         if (empty($input->getArgument('config_name'))) {
404             $config_names = $this->getConfigFactory()->listAll();
405             $choice = $this->io()->choice('Choose a configuration', drush_map_assoc($config_names));
406             $input->setArgument('config_name', $choice);
407         }
408     }
409
410     /**
411      * @hook interact @interact-config-label
412      */
413     public function interactConfigLabel(InputInterface $input, ConsoleOutputInterface $output)
414     {
415         global $config_directories;
416
417         $option_name = $input->hasOption('destination') ? 'destination' : 'source';
418         if (empty($input->getArgument('label') && empty($input->getOption($option_name)))) {
419             $choices = drush_map_assoc(array_keys($config_directories));
420             unset($choices[CONFIG_ACTIVE_DIRECTORY]);
421             if (count($choices) >= 2) {
422                 $label = $this->io()->choice('Choose a '. $option_name. '.', $choices);
423                 $input->setArgument('label', $label);
424             }
425         }
426     }
427
428     /**
429      * Validate that a config name is valid.
430      *
431      * If the argument to be validated is not named $config_name, pass the
432      * argument name as the value of the annotation.
433      *
434      * @hook validate @validate-config-name
435      * @param \Consolidation\AnnotatedCommand\CommandData $commandData
436      * @return \Consolidation\AnnotatedCommand\CommandError|null
437      */
438     public function validateConfigName(CommandData $commandData)
439     {
440         $arg_name = $commandData->annotationData()->get('validate-config-name', null) ?: 'config_name';
441         $config_name = $commandData->input()->getArgument($arg_name);
442         $config = \Drupal::config($config_name);
443         if ($config->isNew()) {
444             $msg = dt('Config !name does not exist', ['!name' => $config_name]);
445             return new CommandError($msg);
446         }
447     }
448
449     /**
450      * Copies configuration objects from source storage to target storage.
451      *
452      * @param StorageInterface $source
453      *   The source config storage service.
454      * @param StorageInterface $destination
455      *   The destination config storage service.
456      */
457     public static function copyConfig(StorageInterface $source, StorageInterface $destination)
458     {
459         // Make sure the source and destination are on the default collection.
460         if ($source->getCollectionName() != StorageInterface::DEFAULT_COLLECTION) {
461             $source = $source->createCollection(StorageInterface::DEFAULT_COLLECTION);
462         }
463         if ($destination->getCollectionName() != StorageInterface::DEFAULT_COLLECTION) {
464             $destination = $destination->createCollection(StorageInterface::DEFAULT_COLLECTION);
465         }
466
467         // Export all the configuration.
468         foreach ($source->listAll() as $name) {
469             $destination->write($name, $source->read($name));
470         }
471
472         // Export configuration collections.
473         foreach ($source->getAllCollectionNames() as $collection) {
474             $source = $source->createCollection($collection);
475             $destination = $destination->createCollection($collection);
476             foreach ($source->listAll() as $name) {
477                 $destination->write($name, $source->read($name));
478             }
479         }
480     }
481
482     /**
483      * Get diff between two config sets.
484      *
485      * @param StorageInterface $destination_storage
486      * @param StorageInterface $source_storage
487      * @param OutputInterface $output
488      * @return array|bool
489      *   An array of strings containing the diff.
490      */
491     public static function getDiff(StorageInterface $destination_storage, StorageInterface $source_storage, OutputInterface $output)
492     {
493         // Copy active storage to a temporary directory.
494         $temp_destination_dir = drush_tempdir();
495         $temp_destination_storage = new FileStorage($temp_destination_dir);
496         self::copyConfig($destination_storage, $temp_destination_storage);
497
498         // Copy source storage to a temporary directory as it could be
499         // modified by the partial option or by decorated sync storages.
500         $temp_source_dir = drush_tempdir();
501         $temp_source_storage = new FileStorage($temp_source_dir);
502         self::copyConfig($source_storage, $temp_source_storage);
503
504         $prefix = 'diff';
505         if (drush_program_exists('git') && $output->isDecorated()) {
506             $prefix = 'git diff --color=always';
507         }
508         drush_shell_exec($prefix . ' -u %s %s', $temp_destination_dir, $temp_source_dir);
509         return drush_shell_exec_output();
510     }
511 }