3bb1e7a00a9d93d2cd750abb314120f1ccfaa92b
[yaffs-website] / vendor / drush / drush / src / SiteAlias / LegacyAliasConverter.php
1 <?php
2 namespace Drush\SiteAlias;
3
4 use Symfony\Component\Yaml\Yaml;
5 use Dflydev\DotAccessData\Data;
6 use Consolidation\SiteAlias\SiteAliasFileDiscovery;
7
8 /**
9  * Find all legacy alias files and convert them to an equivalent '.yml' file.
10  *
11  * We will check the respective mod date of the legacy file and the generated
12  * file, and update the generated file when the legacy file changes.
13  */
14 class LegacyAliasConverter
15 {
16     /**
17      * @var SiteAliasFileDiscovery
18      */
19     protected $discovery;
20
21     /**
22      * @var string
23      */
24     protected $target;
25
26     /**
27      * @var boolean
28      */
29     protected $converted;
30
31     /**
32      * @var boolean
33      */
34     protected $simulate = false;
35
36     /**
37      * @var array
38      */
39     protected $convertedFileMap = [];
40
41     /**
42      * LegacyAliasConverter constructor.
43      *
44      * @param SiteAliasFileDiscovery $discovery Provide the same discovery
45      *   object as used by the SiteAliasFileLoader to ensure that the same
46      *   search locations are used for both classed.
47      */
48     public function __construct(SiteAliasFileDiscovery $discovery)
49     {
50         $this->discovery = $discovery;
51         $this->target = '';
52     }
53
54     /**
55      * @return bool
56      */
57     public function isSimulate()
58     {
59         return $this->simulate;
60     }
61
62     /**
63      * @param bool $simulate
64      */
65     public function setSimulate($simulate)
66     {
67         $this->simulate = $simulate;
68     }
69
70     /**
71      * @param string $target
72      *   A directory to write to. If not provided, writes go into same dir as the corresponding legacy file.
73      */
74     public function setTargetDir($target)
75     {
76         $this->target = $target;
77     }
78
79     public function convertOnce()
80     {
81         if ($this->converted) {
82             return;
83         }
84         return $this->convert();
85     }
86
87     public function convert()
88     {
89         $this->converted = true;
90         $legacyFiles = $this->discovery->findAllLegacyAliasFiles();
91
92         if (!$this->checkAnyNeedsConversion($legacyFiles)) {
93             return [];
94         }
95
96         // We reconvert all legacy files together, because the aliases
97         // in the legacy files might be written into multiple different .yml
98         // files, depending on the naming conventions followed.
99         $convertedFiles = $this->convertAll($legacyFiles);
100         $this->writeAll($convertedFiles);
101
102         return $convertedFiles;
103     }
104
105     protected function checkAnyNeedsConversion($legacyFiles)
106     {
107         foreach ($legacyFiles as $legacyFile) {
108             $convertedFile = $this->determineConvertedFilename($legacyFile);
109             if ($this->checkNeedsConversion($legacyFile, $convertedFile)) {
110                 return true;
111             }
112         }
113         return false;
114     }
115
116     protected function convertAll($legacyFiles)
117     {
118         $result = [];
119         foreach ($legacyFiles as $legacyFile) {
120             $convertedFile = $this->determineConvertedFilename($legacyFile);
121             $conversionResult = $this->convertLegacyFile($legacyFile);
122             $result = static::arrayMergeRecursiveDistinct($result, $conversionResult);
123
124             // If the conversion did not generate a similarly-named .yml file, then
125             // make sure that one is created simply to record the mod date.
126             if (!isset($result[$convertedFile])) {
127                 $result[$convertedFile] = [];
128             }
129         }
130         return $result;
131     }
132
133     protected function writeAll($convertedFiles)
134     {
135         foreach ($convertedFiles as $path => $data) {
136             $contents = $this->getContents($path, $data);
137
138             // Write the converted file to the target directory
139             // if a target directory was set.
140             if (!empty($this->target)) {
141                 $path = $this->target . '/' . basename($path);
142             }
143             $this->writeOne($path, $contents);
144         }
145     }
146
147     protected function getContents($path, $data)
148     {
149         if (!empty($data)) {
150             $indent = 2;
151             return Yaml::dump($data, PHP_INT_MAX, $indent, false, true);
152         }
153
154         $recoverSource = $this->recoverLegacyFile($path);
155         if (!$recoverSource) {
156             $recoverSource = 'the source alias file';
157         }
158         $contents = <<<EOT
159 # This is a placeholder file used to track when $recoverSource was converted.
160 # If you delete $recoverSource, then you may delete this file.
161 EOT;
162
163         return $contents;
164     }
165
166     protected function writeOne($path, $contents)
167     {
168         $checksumPath = $this->checksumPath($path);
169         if ($this->safeToWrite($path, $contents, $checksumPath)) {
170             file_put_contents($path, $contents);
171             $this->saveChecksum($checksumPath, $path, $contents);
172         }
173     }
174
175     /**
176      * Without any safeguards, the conversion process could be very
177      * dangerous to users who modify their converted alias files (as we
178      * would encourage them to do, if the goal is to convert!).
179      *
180      * This method determines whether it is safe to write to the converted
181      * alias file at the specified path. If the user has modified the target
182      * file, then we will not overwrite it.
183      */
184     protected function safeToWrite($path, $contents, $checksumPath)
185     {
186         // Bail if simulate mode is enabled.
187         if ($this->isSimulate()) {
188             return true;
189         }
190
191         // If the target file does not exist, it is always safe to write.
192         if (!file_exists($path)) {
193             return true;
194         }
195
196         // If the user deletes the checksum file, then we will never
197         // overwrite the file again. This also covers potential collisions,
198         // where the user might not realize that a legacy alias file
199         // would write to a new site.yml file they created manually.
200         if (!file_exists($checksumPath)) {
201             return false;
202         }
203
204         // Read the data that exists at the target path, and calculate
205         // the checksum of what exists there.
206         $previousContents = file_get_contents($path);
207         $previousChecksum = $this->calculateChecksum($previousContents);
208         $previousWrittenChecksum = $this->readChecksum($checksumPath);
209
210         // If the checksum of what we wrote before is the same as
211         // the checksum we cached in the checksum file, then there has
212         // been no user modification of this file, and it is safe to
213         // overwrite it.
214         return $previousChecksum == $previousWrittenChecksum;
215     }
216
217     public function saveChecksum($checksumPath, $path, $contents)
218     {
219         $name = basename($path);
220         $comment = <<<EOT
221 # Checksum for converted Drush alias file $name.
222 # Delete this checksum file or modify $name to prevent further updates to it.
223 EOT;
224         $checksum = $this->calculateChecksum($contents);
225         @mkdir(dirname($checksumPath));
226         file_put_contents($checksumPath, "{$comment}\n{$checksum}");
227     }
228
229     protected function readChecksum($checksumPath)
230     {
231         $checksumContents = file_get_contents($checksumPath);
232         $checksumContents = preg_replace('/^#.*/m', '', $checksumContents);
233
234         return trim($checksumContents);
235     }
236
237     protected function checksumPath($path)
238     {
239         return dirname($path) . '/.checksums/' . basename($path, '.yml') . '.md5';
240     }
241
242     protected function calculateChecksum($data)
243     {
244         return md5($data);
245     }
246
247     protected function determineConvertedFilename($legacyFile)
248     {
249         $convertedFile = preg_replace('#\.alias(|es)\.drushrc\.php$#', '.site.yml', $legacyFile);
250         // Sanity check: if no replacement was done on the filesystem, then
251         // we will presume that no conversion is needed here after all.
252         if ($convertedFile == $legacyFile) {
253             return false;
254         }
255         // If a target directory was set, then the converted file will
256         // be written there. This will be done in writeAll(); we will strip
257         // off everything except for the basename here. If no target
258         // directory was set, then we will keep the path to the converted
259         // file so that it may be written to the correct location.
260         if (!empty($this->target)) {
261             $convertedFile = basename($convertedFile);
262         }
263         $this->cacheConvertedFilePath($legacyFile, $convertedFile);
264         return $convertedFile;
265     }
266
267     protected function cacheConvertedFilePath($legacyFile, $convertedFile)
268     {
269         $this->convertedFileMap[basename($convertedFile)] = basename($legacyFile);
270     }
271
272     protected function recoverLegacyFile($convertedFile)
273     {
274         if (!isset($this->convertedFileMap[basename($convertedFile)])) {
275             return false;
276         }
277         return $this->convertedFileMap[basename($convertedFile)];
278     }
279
280     protected function checkNeedsConversion($legacyFile, $convertedFile)
281     {
282         // If determineConvertedFilename did not return a valid result,
283         // then force no conversion.
284         if (!$convertedFile) {
285             return;
286         }
287
288         // Sanity check: the source file must exist.
289         if (!file_exists($legacyFile)) {
290             return false;
291         }
292
293         // If the target file does not exist, then force a conversion
294         if (!file_exists($convertedFile)) {
295             return true;
296         }
297
298         // We need to re-convert if the legacy file has been modified
299         // more recently than the converted file.
300         return filemtime($legacyFile) > filemtime($convertedFile);
301     }
302
303     protected function convertLegacyFile($legacyFile)
304     {
305         $aliases = [];
306         $options = [];
307         // Include the legacy file. In theory, this will define $aliases &/or $options.
308         if (((@include $legacyFile) === false) || (!isset($aliases) && !isset($options))) {
309             // TODO: perhaps we should log a warning?
310             return;
311         }
312
313         // Decide whether this is a single-alias file or a multiple-alias file.
314         if (preg_match('#\.alias\.drushrc\.php$#', $legacyFile)) {
315             return $this->convertSingleAliasLegacyFile($legacyFile, $options ?: current($aliases));
316         }
317         return $this->convertMultipleAliasesLegacyFile($legacyFile, $aliases, $options);
318     }
319
320     protected function convertSingleAliasLegacyFile($legacyFile, $options)
321     {
322         $aliasName = basename($legacyFile, '.alias.drushrc.php');
323
324         return $this->convertAlias($aliasName, $options, dirname($legacyFile));
325     }
326
327     protected function convertMultipleAliasesLegacyFile($legacyFile, $aliases, $options)
328     {
329         $result = [];
330         foreach ($aliases as $aliasName => $data) {
331             // 'array_merge' is how Drush 8 combines these records.
332             $data = array_merge($options, $data);
333             $convertedAlias = $this->convertAlias($aliasName, $data, dirname($legacyFile));
334             $result = static::arrayMergeRecursiveDistinct($result, $convertedAlias);
335         }
336         return $result;
337     }
338
339     protected function convertAlias($aliasName, $data, $dir = '')
340     {
341         $env = 'dev';
342         // We allow $aliasname to be:
343         //   - sitename
344         //   - sitename.env
345         //   - group.sitename.env
346         // In the case of the last, we will convert to
347         // 'group-sitename.env' (and so on for any additional dots).
348         // First, we will strip off the 'env' if it is present.
349         if (preg_match('/(.*)\.([^.]+)$/', $aliasName, $matches)) {
350             $aliasName = $matches[1];
351             $env = $matches[2];
352         }
353         // Convert all remaining dots to dashes.
354         $aliasName = strtr($aliasName, '.', '-');
355
356         $data = $this->fixSiteData($data);
357
358         return $this->convertSingleFileAlias($aliasName, $env, $data, $dir);
359     }
360
361     protected function fixSiteData($data)
362     {
363         $keyMap = $this->keyConversion();
364
365         $options = [];
366         foreach ($data as $key => $value) {
367             if ($key[0] == '#') {
368                 unset($data[$key]);
369             } elseif (!isset($keyMap[$key])) {
370                 $options[$key] = $data[$key];
371                 unset($data[$key]);
372             }
373         }
374         ksort($options);
375
376         foreach ($keyMap as $fromKey => $toKey) {
377             if (isset($data[$fromKey]) && ($fromKey != $toKey)) {
378                 $data[$toKey] = $data[$fromKey];
379                 unset($data[$fromKey]);
380             }
381         }
382
383         if (!empty($options)) {
384             $data['options'] = $options;
385         }
386         if (isset($data['paths'])) {
387             $data['paths'] = $this->removePercentFromKey($data['paths']);
388         }
389         ksort($data);
390
391         return $this->remapData($data);
392     }
393
394     protected function remapData($data)
395     {
396         $converter = new Data($data);
397
398         foreach ($this->dataRemap() as $from => $to) {
399             if ($converter->has($from)) {
400                 $converter->set($to, $converter->get($from));
401                 $converter->remove($from);
402             }
403         }
404
405         return $converter->export();
406     }
407
408     /**
409      * Anything in the key of the returned array is converted
410      * and written to a new top-level item in the result.
411      *
412      * Anything NOT identified by the key in the returned array
413      * is moved to the 'options' element.
414      */
415     protected function keyConversion()
416     {
417         return [
418             'remote-host' => 'host',
419             'remote-user' => 'user',
420             'root' => 'root',
421             'uri' => 'uri',
422             'path-aliases' => 'paths',
423         ];
424     }
425
426     /**
427      * This table allows for flexible remapping from one location
428      * in the original alias to any other location in the target
429      * alias.
430      *
431      * n.b. Most arbitrary data from the original alias will have
432      * been moved into the 'options' element before this remapping
433      * table is consulted.
434      */
435     protected function dataRemap()
436     {
437         return [
438             'options.ssh-options' => 'ssh.options',
439         ];
440     }
441
442     protected function removePercentFromKey($data)
443     {
444         return
445             array_flip(
446                 array_map(
447                     function ($item) {
448                         return ltrim($item, '%');
449                     },
450                     array_flip($data)
451                 )
452             );
453     }
454
455     protected function convertSingleFileAlias($aliasName, $env, $data, $dir = '')
456     {
457         $filename = $this->outputFilename($aliasName, '.site.yml', $dir);
458         return [
459             $filename => [
460                 $env => $data,
461             ],
462         ];
463     }
464
465     protected function outputFilename($name, $extension, $dir = '')
466     {
467         $filename = "{$name}{$extension}";
468         // Just return the filename part if no directory was provided. Also,
469         // the directoy is irrelevant if a target directory is set.
470         if (empty($dir) || !empty($this->target)) {
471             return $filename;
472         }
473         return "$dir/$filename";
474     }
475
476     /**
477      * Merges arrays recursively while preserving.
478      *
479      * @param array $array1
480      * @param array $array2
481      *
482      * @return array
483      *
484      * @see http://php.net/manual/en/function.array-merge-recursive.php#92195
485      * @see https://github.com/grasmash/bolt/blob/robo-rebase/src/Robo/Common/ArrayManipulator.php#L22
486      */
487     protected static function arrayMergeRecursiveDistinct(
488         array &$array1,
489         array &$array2
490     ) {
491         $merged = $array1;
492         foreach ($array2 as $key => &$value) {
493             $merged[$key] = self::mergeRecursiveValue($merged, $key, $value);
494         }
495         ksort($merged);
496         return $merged;
497     }
498
499     /**
500      * Process the value in an arrayMergeRecursiveDistinct - make a recursive
501      * call if needed.
502      */
503     private static function mergeRecursiveValue(&$merged, $key, $value)
504     {
505         if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
506             return self::arrayMergeRecursiveDistinct($merged[$key], $value);
507         }
508         return $value;
509     }
510 }