Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / vendor / drush / drush / src / Config / ConfigLocator.php
1 <?php
2 namespace Drush\Config;
3
4 use Consolidation\Config\Loader\ConfigLoaderInterface;
5 use Drush\Config\Loader\YamlConfigLoader;
6 use Consolidation\Config\Loader\ConfigProcessor;
7 use Consolidation\Config\Util\EnvConfig;
8 use Symfony\Component\Finder\Finder;
9
10 /**
11  * Locate Drush configuration files and load them into the configuration
12  * instance.
13  *
14  * This class knows how to find all of the global and site-local
15  * configuration files for Drush, as long as it is provided with
16  * the necessary base directories:
17  *
18  * - The user's home directory
19  * - The values provided for --config and --alias-path
20  * - The Drupal root
21  *
22  * There are two operating modes that are supported:
23  *
24  * - Normal: All config locations are used.
25  * - Local:  The global locations are omitted.
26  *
27  * The mode is set via the `setLocal()` method.
28  */
29 class ConfigLocator
30 {
31     /**
32      * @var \Robo\Config
33      */
34     protected $config;
35
36     protected $isLocal;
37
38     protected $sources = false;
39
40     protected $siteRoots = [];
41
42     protected $composerRoot;
43
44     protected $configFilePaths = [];
45
46     protected $configFileVariant;
47
48     protected $processedConfigPaths = [];
49
50     /*
51      * From context.inc:
52      *
53      *   Specified by the script itself :
54      *     process  : Generated in the current process.
55      *     cli      : Passed as --option=value to the command line.
56      *     stdin    : Passed as a JSON encoded string through stdin.
57      *     specific : Defined in a command-specific option record, and
58      *                set in the command context whenever that command is used.
59      *     alias    : Defined in an alias record, and set in the
60      *                alias context whenever that alias is used.
61      *
62      *   Specified by config files :
63      *     custom   : Loaded from the config file specified by --config or -c
64      *     site     : Loaded from the drush.yml file in the Drupal site directory.
65      *     drupal   : Loaded from the drush.yml file in the Drupal root directory.
66      *     user     : Loaded from the drush.yml file in the user's home directory.
67      *     home.drush Loaded from the drush.yml file in the $HOME/.drush directory.
68      *     system   : Loaded from the drush.yml file in the system's $PREFIX/etc/drush directory.
69      *     drush    : Loaded from the drush.yml file in the same directory as drush.php.
70      *
71      *   Specified by the script, but has the lowest priority :
72      *     default  : The script might provide some sensible defaults during init.
73      */
74
75     // 'process' context is provided by ConfigOverlay
76     const ENVIRONMENT_CONTEXT = 'environment'; // This is more of a 'runtime' context
77     const PREFLIGHT_CONTEXT = 'cli';
78     // 'stdin' context not implemented
79     // 'specific' context obsolete; command-specific options handled differently by annotated command library
80     const ALIAS_CONTEXT = 'alias';
81     // custom context is obsolete (loaded in USER_CONTEXT)
82     const SITE_CONTEXT = 'site';
83     const DRUPAL_CONTEXT = 'drupal';
84     const USER_CONTEXT = 'user';
85     // home.drush is obsolete (loaded in USER_CONTEXT)
86     // system context is obsolete (loaded in USER_CONTEXT - note priority change)
87     const ENV_CONTEXT = 'env';
88     const DRUSH_CONTEXT = 'drush';
89
90     // 'default' context is provided by ConfigOverlay
91
92     /**
93      * ConfigLocator constructor
94      */
95     public function __construct($envPrefix = '', $configFileVariant = '')
96     {
97         $this->configFileVariant = $configFileVariant;
98         $this->config = new DrushConfig();
99
100         // Add placeholders to establish priority. We add
101         // contexts from lowest to highest priority.
102         $this->config->addPlaceholder(self::DRUSH_CONTEXT);
103         if (!empty($envPrefix)) {
104             $envConfig = new EnvConfig($envPrefix);
105             $this->config->addContext(self::ENV_CONTEXT, $envConfig);
106         }
107         $this->config->addPlaceholder(self::USER_CONTEXT);
108         $this->config->addPlaceholder(self::DRUPAL_CONTEXT);
109         $this->config->addPlaceholder(self::SITE_CONTEXT); // not implemented yet (multisite)
110         $this->config->addPlaceholder(self::ALIAS_CONTEXT);
111         $this->config->addPlaceholder(self::PREFLIGHT_CONTEXT);
112         $this->config->addPlaceholder(self::ENVIRONMENT_CONTEXT);
113
114         $this->isLocal = false;
115
116         $this->configFilePaths = [];
117     }
118
119     /**
120      * Put the config locator into 'local 'mode.
121      *
122      * @param bool $isLocal
123      */
124     public function setLocal($isLocal)
125     {
126         $this->isLocal = $isLocal;
127     }
128
129     /**
130      * Keep track of the source that every config item originally came from.
131      * Potentially useful in debugging.  If collectSources(true) is called,
132      * then the sources will be accumulated as config files are loaded. Otherwise,
133      * this information will not be saved.
134      *
135      * @param bool $collect
136      * @return $this
137      */
138     public function collectSources($collect = true)
139     {
140         $this->sources = $collect ? [] : false;
141         return $this;
142     }
143
144     /**
145      * Return all of the sources for every configuration item. The key
146      * is the address of the configuration item, and the value is the
147      * configuration file it was loaded from. Note that this method will
148      * return just an empty array unless collectSources(true) is called
149      * prior to loading configuration files.
150      *
151      * @return array
152      */
153     public function sources()
154     {
155         return $this->sources;
156     }
157
158     /**
159      * Return a list of all configuration files that were loaded.
160      *
161      * @return string[]
162      */
163     public function configFilePaths()
164     {
165         return $this->configFilePaths;
166     }
167
168     /**
169      * Accumulate the sources provided by the configuration loader.
170      */
171     protected function addToSources(array $sources)
172     {
173         if (!is_array($this->sources)) {
174             return;
175         }
176         $this->sources = array_merge_recursive($this->sources, $sources);
177     }
178
179     /**
180      * Return the configuration object. Create it and load it with
181      * all identified configuration if necessary.
182      *
183      * @return Config
184      */
185     public function config()
186     {
187         return $this->config;
188     }
189
190     /**
191      * Exports all of the information stored in the environment, and adds
192      * it to the configuration.  The Environment object itself is only
193      * available during preflight; the information exported here may be
194      * obtained by commands et. al. as needed. @see Environment::exportConfigData()
195      *
196      * @param Environment $environent
197      * @return $this
198      */
199     public function addEnvironment(Environment $environment)
200     {
201         $this->config->getContext(self::ENVIRONMENT_CONTEXT)->import($environment->exportConfigData());
202         return $this;
203     }
204
205     /**
206      *  Add config paths defined in preflight configuration.
207      *
208      * @param array $paths
209      * @return $this
210      */
211     public function addPreflightConfigFiles($filepaths)
212     {
213         $this->addConfigPaths(self::PREFLIGHT_CONTEXT, (array) $filepaths);
214         return $this;
215     }
216
217     /**
218      * Take any configuration from the active alias record, and add it
219      * to our configuratino.
220      * @return $this
221      */
222     public function addAliasConfig($aliasConfig)
223     {
224         $this->config->addContext(self::ALIAS_CONTEXT, $aliasConfig);
225         return $this;
226     }
227
228
229     /**
230      * Given the path provided via --config and the user's home directory,
231      * add all of the user configuration paths.
232      *
233      * In 'local' mode, only the --config location is used.
234      * @return $this
235      */
236     public function addUserConfig($configPaths, $systemConfigPath, $userConfigDir)
237     {
238         $paths = $configPaths;
239         if (!$this->isLocal) {
240             $paths = array_merge($paths, [ $systemConfigPath, $userConfigDir ]);
241         }
242         $this->addConfigPaths(self::USER_CONTEXT, $paths);
243         return $this;
244     }
245
246     /**
247      * Add the Drush project directory as a configuration search location.
248      *
249      * @param $drushProjectDir path to the drush project directory
250      * @return $this
251      */
252     public function addDrushConfig($drushProjectDir)
253     {
254         if (!$this->isLocal) {
255             $this->addConfigPaths(self::DRUSH_CONTEXT, [ $drushProjectDir ]);
256         }
257         return $this;
258     }
259
260     /**
261      * Add any configuration files found around the Drupal root of the
262      * selected site.
263      *
264      * @param Path to the selected Drupal site
265      * @return $this
266      */
267     public function addSitewideConfig($siteRoot)
268     {
269         // There might not be a site.
270         if (!is_dir($siteRoot)) {
271             return;
272         }
273
274         // We might have already processed this root.
275         $siteRoot = realpath($siteRoot);
276         if (in_array($siteRoot, $this->siteRoots)) {
277             return;
278         }
279
280         // Remember that we've seen this location.
281         $this->siteRoots[] = $siteRoot;
282
283         $this->addConfigPaths(self::DRUPAL_CONTEXT, [ dirname($siteRoot) . '/drush', "$siteRoot/drush", "$siteRoot/sites/all/drush" ]);
284         return $this;
285     }
286
287     /**
288      * Add any configuration file found at any of the provided paths. Both the
289      * provided location, and the directory `config` inside each provided location
290      * is searched for a drush.yml file.
291      *
292      * @param string $contextName Which context to put all configuration files in.
293      * @param string[] $paths List of paths to search for configuration.
294      * @return $this
295      */
296     public function addConfigPaths($contextName, $paths)
297     {
298         $loader = new YamlConfigLoader();
299         // Make all of the config values parsed so far available in evaluations.
300         $reference = $this->config()->export();
301         $processor = new ConfigProcessor();
302         $context = $this->config->getContext($contextName);
303         $processor->add($context->export());
304
305         $candidates = [
306             'drush.yml',
307         ];
308         if ($this->configFileVariant) {
309             $candidates[] = "drush{$this->configFileVariant}.yml";
310         }
311         $candidates = $this->expandCandidates($candidates, 'config/');
312         $config_files = $this->findConfigFiles($paths, $candidates);
313         $this->addConfigFiles($processor, $loader, $config_files);
314
315         // Complete config import.
316         $this->addToSources($processor->sources());
317         $context->import($processor->export($reference));
318         $this->config->addContext($contextName, $context);
319         $this->processedConfigPaths = array_merge($this->processedConfigPaths, $paths);
320
321         // Recursive case.
322         if ($context->has('drush.paths.config')) {
323             $new_config_paths = array_diff((array) $context->get('drush.paths.config'), $this->processedConfigPaths);
324             if ($new_config_paths) {
325                 $this->addConfigPaths($contextName, $new_config_paths);
326             }
327         }
328
329         return $this;
330     }
331
332     /**
333      * Adds $configFiles config files.
334      *
335      * @param ConfigProcessor $processor
336      * @param ConfigLoaderInterface $loader
337      * @param array $configFiles
338      */
339     protected function addConfigFiles(ConfigProcessor $processor, ConfigLoaderInterface $loader, array $configFiles)
340     {
341         foreach ($configFiles as $configFile) {
342             $processor->extend($loader->load($configFile));
343             $this->configFilePaths[] = $configFile;
344         }
345     }
346
347     /**
348      * Given a list of paths, and candidates that might exist at each path,
349      * return all of the candidates that can be found. Candidates may be
350      * either directories or files.
351      *
352      * @param string[] $paths
353      * @param string[] $candidates
354      * @return string[] paths
355      */
356     protected function identifyCandidates($paths, $candidates)
357     {
358         $configFiles = [];
359         foreach ($paths as $path) {
360             $configFiles = array_merge($configFiles, $this->identifyCandidatesAtPath($path, $candidates));
361         }
362         return $configFiles;
363     }
364
365     /**
366      * Search for all matching candidate locations at a single path.
367      * Candidate locations may be either directories or files.
368      *
369      * @param string $path
370      * @param string[] $candidates
371      * @return string[]
372      */
373     protected function identifyCandidatesAtPath($path, $candidates)
374     {
375         if (!is_dir($path)) {
376             return [];
377         }
378
379         $result = [];
380         foreach ($candidates as $candidate) {
381             $configFile = empty($candidate) ? $path : "$path/$candidate";
382             if (file_exists($configFile)) {
383                 $result[] = $configFile;
384             }
385         }
386         return $result;
387     }
388
389     /**
390      * Get the site aliases according to preflight arguments and environment.
391      *
392      * @param $preflightArgs
393      * @param Environment $environment
394      *
395      * @return array
396      */
397     public function getSiteAliasPaths($paths, Environment $environment)
398     {
399         // In addition to the paths passed in to us (from --alias-paths
400         // commandline options), add some site-local locations.
401         $base_dirs = array_filter(array_merge($this->siteRoots, [$this->composerRoot]));
402         $site_local_paths = array_map(
403             function ($item) {
404                 return "$item/drush/sites";
405             },
406             $base_dirs
407         );
408         $paths = array_merge($paths, $site_local_paths);
409
410         return $paths;
411     }
412
413     /**
414      * Get the commandfile paths according to preflight arguments.
415      *
416      * @param $preflightArgs
417      *
418      * @return array
419      */
420     public function getCommandFilePaths($commandPaths, $root)
421     {
422         $builtin = $this->getBuiltinCommandFilePaths();
423         $included = $this->getIncludedCommandFilePaths($commandPaths);
424         $site = $this->getSiteCommandFilePaths(["$root/drush", dirname($root) . '/drush']);
425
426         return array_merge(
427             $builtin,
428             $included,
429             $site
430         );
431     }
432
433     /**
434      * Return all of the built-in commandfile locations
435      */
436     protected function getBuiltinCommandFilePaths()
437     {
438         return [
439             dirname(__DIR__),
440         ];
441     }
442
443     /**
444      * Return all of the commandfile locations specified via
445      * an 'include' option.
446      */
447     protected function getIncludedCommandFilePaths($commandPaths)
448     {
449         $searchpath = [];
450         // Commands specified by 'include' option
451         foreach ($commandPaths as $commandPath) {
452             if (is_dir($commandPath)) {
453                 $searchpath[] = $commandPath;
454             }
455         }
456         return $searchpath;
457     }
458
459     /**
460      * Return all of the commandfile paths in any '$root/drush' or
461      * 'dirname($root)/drush' directory that contains a composer.json
462      * file or a 'Commands' or 'src/Commands' directory.
463      */
464     protected function getSiteCommandFilePaths($directories)
465     {
466         $result = [];
467
468         $directories = array_filter($directories, 'is_dir');
469
470         if (empty($directories)) {
471             return $result;
472         }
473
474         // Find projects
475         $finder = new Finder();
476         $finder->files()
477             ->ignoreUnreadableDirs()
478             ->path('#composer.json$|^src/Commands|^Commands#')
479             ->in($directories)
480             ->depth('<= 3');
481
482         foreach ($finder as $file) {
483             $result[] = dirname($file->getRealPath());
484         }
485
486         return $result;
487     }
488
489     /**
490      * Sets the composer root.
491      *
492      * @param $selectedComposerRoot
493      */
494     public function setComposerRoot($selectedComposerRoot)
495     {
496         $this->composerRoot = $selectedComposerRoot;
497     }
498
499     /**
500      * Double the candidates, adding '$prefix' before each existing one.
501      */
502     public function expandCandidates($candidates, $prefix)
503     {
504         $additional = array_map(
505             function ($item) use ($prefix) {
506                 return $prefix . $item;
507             },
508             $candidates
509         );
510         return array_merge($candidates, $additional);
511     }
512
513     /**
514      * Given an array of paths, separates files and directories.
515      *
516      * @param array $paths
517      *   An array of config paths. These may be config files or paths to dirs
518      *   containing config files.
519      * @param array $candidates
520      *   An array filenames that are considered config files.
521      *
522      * @return array
523      *   An array. The first row is an array of files, the second row is an
524      *   array of dirs.
525      */
526     protected function findConfigFiles($paths, $candidates)
527     {
528         $files = [];
529         $dirs = [];
530         foreach ($paths as $path) {
531             if (file_exists($path)) {
532                 if (is_dir($path)) {
533                     $dirs[] = realpath($path);
534                 } else {
535                     $files[] = realpath($path);
536                 }
537             }
538         }
539
540         // Search directories for config file candidates.
541         $discovered_config_files = $this->identifyCandidates($dirs, $candidates);
542
543         // Merge discoverd candidates with explicitly specified config files.
544         $config_files = array_merge($discovered_config_files, $files);
545
546         return $config_files;
547     }
548 }