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