4140fd406efe9d4b625fc7e8b1dad0a2f5d5c5cf
[yaffs-website] / vendor / cweagans / composer-patches / src / Patches.php
1 <?php
2
3 /**
4  * @file
5  * Provides a way to patch Composer packages after installation.
6  */
7
8 namespace cweagans\Composer;
9
10 use Composer\Composer;
11 use Composer\DependencyResolver\Operation\InstallOperation;
12 use Composer\DependencyResolver\Operation\UninstallOperation;
13 use Composer\DependencyResolver\Operation\UpdateOperation;
14 use Composer\DependencyResolver\Operation\OperationInterface;
15 use Composer\EventDispatcher\EventSubscriberInterface;
16 use Composer\IO\IOInterface;
17 use Composer\Package\AliasPackage;
18 use Composer\Package\PackageInterface;
19 use Composer\Plugin\PluginInterface;
20 use Composer\Installer\PackageEvents;
21 use Composer\Script\Event;
22 use Composer\Script\ScriptEvents;
23 use Composer\Installer\PackageEvent;
24 use Composer\Util\ProcessExecutor;
25 use Composer\Util\RemoteFilesystem;
26 use Symfony\Component\Process\Process;
27
28 class Patches implements PluginInterface, EventSubscriberInterface {
29
30   /**
31    * @var Composer $composer
32    */
33   protected $composer;
34   /**
35    * @var IOInterface $io
36    */
37   protected $io;
38   /**
39    * @var EventDispatcher $eventDispatcher
40    */
41   protected $eventDispatcher;
42   /**
43    * @var ProcessExecutor $executor
44    */
45   protected $executor;
46   /**
47    * @var array $patches
48    */
49   protected $patches;
50
51   /**
52    * Apply plugin modifications to composer
53    *
54    * @param Composer    $composer
55    * @param IOInterface $io
56    */
57   public function activate(Composer $composer, IOInterface $io) {
58     $this->composer = $composer;
59     $this->io = $io;
60     $this->eventDispatcher = $composer->getEventDispatcher();
61     $this->executor = new ProcessExecutor($this->io);
62     $this->patches = array();
63     $this->installedPatches = array();
64   }
65
66   /**
67    * Returns an array of event names this subscriber wants to listen to.
68    */
69   public static function getSubscribedEvents() {
70     return array(
71       ScriptEvents::PRE_INSTALL_CMD => "checkPatches",
72       ScriptEvents::PRE_UPDATE_CMD => "checkPatches",
73       PackageEvents::PRE_PACKAGE_INSTALL => "gatherPatches",
74       PackageEvents::PRE_PACKAGE_UPDATE => "gatherPatches",
75       PackageEvents::POST_PACKAGE_INSTALL => "postInstall",
76       PackageEvents::POST_PACKAGE_UPDATE => "postInstall",
77     );
78   }
79
80   /**
81    * Before running composer install,
82    * @param Event $event
83    */
84   public function checkPatches(Event $event) {
85     if (!$this->isPatchingEnabled()) {
86       return;
87     }
88
89     try {
90       $repositoryManager = $this->composer->getRepositoryManager();
91       $localRepository = $repositoryManager->getLocalRepository();
92       $installationManager = $this->composer->getInstallationManager();
93       $packages = $localRepository->getPackages();
94
95       $tmp_patches = $this->grabPatches();
96       if ($tmp_patches == FALSE) {
97         $this->io->write('<info>No patches supplied.</info>');
98         return;
99       }
100
101       foreach ($packages as $package) {
102         $extra = $package->getExtra();
103         if (isset($extra['patches'])) {
104           $this->installedPatches[$package->getName()] = $extra['patches'];
105         }
106         $patches = isset($extra['patches']) ? $extra['patches'] : array();
107         $tmp_patches = array_merge_recursive($tmp_patches, $patches);
108       }
109
110       // Remove packages for which the patch set has changed.
111       foreach ($packages as $package) {
112         if (!($package instanceof AliasPackage)) {
113           $package_name = $package->getName();
114           $extra = $package->getExtra();
115           $has_patches = isset($tmp_patches[$package_name]);
116           $has_applied_patches = isset($extra['patches_applied']);
117           if (($has_patches && !$has_applied_patches)
118             || (!$has_patches && $has_applied_patches)
119             || ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) {
120             $uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.');
121             $this->io->write('<info>Removing package ' . $package_name . ' so that it can be re-installed and re-patched.</info>');
122             $installationManager->uninstall($localRepository, $uninstallOperation);
123           }
124         }
125       }
126     }
127       // If the Locker isn't available, then we don't need to do this.
128       // It's the first time packages have been installed.
129     catch (\LogicException $e) {
130       return;
131     }
132   }
133
134   /**
135    * Gather patches from dependencies and store them for later use.
136    *
137    * @param PackageEvent $event
138    */
139   public function gatherPatches(PackageEvent $event) {
140     // If we've already done this, then don't do it again.
141     if (isset($this->patches['_patchesGathered'])) {
142       $this->io->write('<info>Patches already gathered. Skipping</info>', TRUE, IOInterface::VERBOSE);
143       return;
144     }
145     // If patching has been disabled, bail out here.
146     elseif (!$this->isPatchingEnabled()) {
147       $this->io->write('<info>Patching is disabled. Skipping.</info>', TRUE, IOInterface::VERBOSE);
148       return;
149     }
150
151     $this->patches = $this->grabPatches();
152     if (empty($this->patches)) {
153       $this->io->write('<info>No patches supplied.</info>');
154     }
155
156     $extra = $this->composer->getPackage()->getExtra();
157     $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array();
158
159     // Now add all the patches from dependencies that will be installed.
160     $operations = $event->getOperations();
161     $this->io->write('<info>Gathering patches for dependencies. This might take a minute.</info>');
162     foreach ($operations as $operation) {
163       if ($operation->getJobType() == 'install' || $operation->getJobType() == 'update') {
164         $package = $this->getPackageFromOperation($operation);
165         $extra = $package->getExtra();
166         if (isset($extra['patches'])) {
167           if (isset($patches_ignore[$package->getName()])) {
168             foreach ($patches_ignore[$package->getName()] as $package_name => $patches) {
169               if (isset($extra['patches'][$package_name])) {
170                 $extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches);
171               }
172             }
173           }
174           $this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $extra['patches']);
175         }
176         // Unset installed patches for this package
177         if(isset($this->installedPatches[$package->getName()])) {
178           unset($this->installedPatches[$package->getName()]);
179         }
180       }
181     }
182
183     // Merge installed patches from dependencies that did not receive an update.
184     foreach ($this->installedPatches as $patches) {
185       $this->patches = array_merge_recursive($this->patches, $patches);
186     }
187
188     // If we're in verbose mode, list the projects we're going to patch.
189     if ($this->io->isVerbose()) {
190       foreach ($this->patches as $package => $patches) {
191         $number = count($patches);
192         $this->io->write('<info>Found ' . $number . ' patches for ' . $package . '.</info>');
193       }
194     }
195
196     // Make sure we don't gather patches again. Extra keys in $this->patches
197     // won't hurt anything, so we'll just stash it there.
198     $this->patches['_patchesGathered'] = TRUE;
199   }
200
201   /**
202    * Get the patches from root composer or external file
203    * @return Patches
204    * @throws \Exception
205    */
206   public function grabPatches() {
207       // First, try to get the patches from the root composer.json.
208     $extra = $this->composer->getPackage()->getExtra();
209     if (isset($extra['patches'])) {
210       $this->io->write('<info>Gathering patches for root package.</info>');
211       $patches = $extra['patches'];
212       return $patches;
213     }
214     // If it's not specified there, look for a patches-file definition.
215     elseif (isset($extra['patches-file'])) {
216       $this->io->write('<info>Gathering patches from patch file.</info>');
217       $patches = file_get_contents($extra['patches-file']);
218       $patches = json_decode($patches, TRUE);
219       $error = json_last_error();
220       if ($error != 0) {
221         switch ($error) {
222           case JSON_ERROR_DEPTH:
223             $msg = ' - Maximum stack depth exceeded';
224             break;
225           case JSON_ERROR_STATE_MISMATCH:
226             $msg =  ' - Underflow or the modes mismatch';
227             break;
228           case JSON_ERROR_CTRL_CHAR:
229             $msg = ' - Unexpected control character found';
230             break;
231           case JSON_ERROR_SYNTAX:
232             $msg =  ' - Syntax error, malformed JSON';
233             break;
234           case JSON_ERROR_UTF8:
235             $msg =  ' - Malformed UTF-8 characters, possibly incorrectly encoded';
236             break;
237           default:
238             $msg =  ' - Unknown error';
239             break;
240           }
241           throw new \Exception('There was an error in the supplied patches file:' . $msg);
242         }
243       if (isset($patches['patches'])) {
244         $patches = $patches['patches'];
245         return $patches;
246       }
247       elseif(!$patches) {
248         throw new \Exception('There was an error in the supplied patch file');
249       }
250     }
251     else {
252       return array();
253     }
254   }
255
256   /**
257    * @param PackageEvent $event
258    * @throws \Exception
259    */
260   public function postInstall(PackageEvent $event) {
261     // Get the package object for the current operation.
262     $operation = $event->getOperation();
263     /** @var PackageInterface $package */
264     $package = $this->getPackageFromOperation($operation);
265     $package_name = $package->getName();
266
267     if (!isset($this->patches[$package_name])) {
268       if ($this->io->isVerbose()) {
269         $this->io->write('<info>No patches found for ' . $package_name . '.</info>');
270       }
271       return;
272     }
273     $this->io->write('  - Applying patches for <info>' . $package_name . '</info>');
274
275     // Get the install path from the package object.
276     $manager = $event->getComposer()->getInstallationManager();
277     $install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
278
279     // Set up a downloader.
280     $downloader = new RemoteFilesystem($this->io, $this->composer->getConfig());
281
282     // Track applied patches in the package info in installed.json
283     $localRepository = $this->composer->getRepositoryManager()->getLocalRepository();
284     $localPackage = $localRepository->findPackage($package_name, $package->getVersion());
285     $extra = $localPackage->getExtra();
286     $extra['patches_applied'] = array();
287
288     foreach ($this->patches[$package_name] as $description => $url) {
289       $this->io->write('    <info>' . $url . '</info> (<comment>' . $description. '</comment>)');
290       try {
291         $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description));
292         $this->getAndApplyPatch($downloader, $install_path, $url);
293         $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description));
294         $extra['patches_applied'][$description] = $url;
295       }
296       catch (\Exception $e) {
297         $this->io->write('   <error>Could not apply patch! Skipping. The error was: ' . $e->getMessage() . '</error>');
298         $extra = $this->composer->getPackage()->getExtra();
299         if (getenv('COMPOSER_EXIT_ON_PATCH_FAILURE') || !empty($extra['composer-exit-on-patch-failure'])) {
300           throw new \Exception("Cannot apply patch $description ($url)!");
301         }
302       }
303     }
304     $localPackage->setExtra($extra);
305
306     $this->io->write('');
307     $this->writePatchReport($this->patches[$package_name], $install_path);
308   }
309
310   /**
311    * Get a Package object from an OperationInterface object.
312    *
313    * @param OperationInterface $operation
314    * @return PackageInterface
315    * @throws \Exception
316    */
317   protected function getPackageFromOperation(OperationInterface $operation) {
318     if ($operation instanceof InstallOperation) {
319       $package = $operation->getPackage();
320     }
321     elseif ($operation instanceof UpdateOperation) {
322       $package = $operation->getTargetPackage();
323     }
324     else {
325       throw new \Exception('Unknown operation: ' . get_class($operation));
326     }
327
328     return $package;
329   }
330
331   /**
332    * Apply a patch on code in the specified directory.
333    *
334    * @param RemoteFilesystem $downloader
335    * @param $install_path
336    * @param $patch_url
337    * @throws \Exception
338    */
339   protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url) {
340
341     // Local patch file.
342     if (file_exists($patch_url)) {
343       $filename = realpath($patch_url);
344     }
345     else {
346       // Generate random (but not cryptographically so) filename.
347       $filename = uniqid(sys_get_temp_dir().'/') . ".patch";
348
349       // Download file from remote filesystem to this location.
350       $hostname = parse_url($patch_url, PHP_URL_HOST);
351       $downloader->copy($hostname, $patch_url, $filename, FALSE);
352     }
353
354     // Modified from drush6:make.project.inc
355     $patched = FALSE;
356     // The order here is intentional. p1 is most likely to apply with git apply.
357     // p0 is next likely. p2 is extremely unlikely, but for some special cases,
358     // it might be useful.
359     $patch_levels = array('-p1', '-p0', '-p2');
360     foreach ($patch_levels as $patch_level) {
361       $checked = $this->executeCommand('cd %s && git --git-dir=. apply --check %s %s', $install_path, $patch_level, $filename);
362       if ($checked) {
363         // Apply the first successful style.
364         $patched = $this->executeCommand('cd %s && git --git-dir=. apply %s %s', $install_path, $patch_level, $filename);
365         break;
366       }
367     }
368
369     // In some rare cases, git will fail to apply a patch, fallback to using
370     // the 'patch' command.
371     if (!$patched) {
372       foreach ($patch_levels as $patch_level) {
373         // --no-backup-if-mismatch here is a hack that fixes some
374         // differences between how patch works on windows and unix.
375         if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) {
376           break;
377         }
378       }
379     }
380
381     // Clean up the temporary patch file.
382     if (isset($hostname)) {
383       unlink($filename);
384     }
385     // If the patch *still* isn't applied, then give up and throw an Exception.
386     // Otherwise, let the user know it worked.
387     if (!$patched) {
388       throw new \Exception("Cannot apply patch $patch_url");
389     }
390   }
391
392   /**
393    * Checks if the root package enables patching.
394    *
395    * @return bool
396    *   Whether patching is enabled. Defaults to TRUE.
397    */
398   protected function isPatchingEnabled() {
399     $extra = $this->composer->getPackage()->getExtra();
400
401     if (empty($extra['patches']) && empty($extra['patches-ignore']) && !isset($extra['patches-file'])) {
402       // The root package has no patches of its own, so only allow patching if
403       // it has specifically opted in.
404       return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE;
405     }
406     else {
407       return TRUE;
408     }
409   }
410
411   /**
412    * Writes a patch report to the target directory.
413    *
414    * @param array $patches
415    * @param string $directory
416    */
417   protected function writePatchReport($patches, $directory) {
418     $output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n";
419     $output .= "Patches applied to this directory:\n\n";
420     foreach ($patches as $description => $url) {
421       $output .= $description . "\n";
422       $output .= 'Source: ' . $url . "\n\n\n";
423     }
424     file_put_contents($directory . "/PATCHES.txt", $output);
425   }
426
427   /**
428    * Executes a shell command with escaping.
429    *
430    * @param string $cmd
431    * @return bool
432    */
433   protected function executeCommand($cmd) {
434     // Shell-escape all arguments except the command.
435     $args = func_get_args();
436     foreach ($args as $index => $arg) {
437       if ($index !== 0) {
438         $args[$index] = escapeshellarg($arg);
439       }
440     }
441
442     // And replace the arguments.
443     $command = call_user_func_array('sprintf', $args);
444     $output = '';
445     if ($this->io->isVerbose()) {
446       $this->io->write('<comment>' . $command . '</comment>');
447       $io = $this->io;
448       $output = function ($type, $data) use ($io) {
449         if ($type == Process::ERR) {
450           $io->write('<error>' . $data . '</error>');
451         }
452         else {
453           $io->write('<comment>' . $data . '</comment>');
454         }
455       };
456     }
457     return ($this->executor->execute($command, $output) == 0);
458   }
459
460   /**
461    * Recursively merge arrays without changing data types of values.
462    *
463    * Does not change the data types of the values in the arrays. Matching keys'
464    * values in the second array overwrite those in the first array, as is the
465    * case with array_merge.
466    *
467    * @param array $array1
468    *   The first array.
469    * @param array $array2
470    *   The second array.
471    * @return array
472    *   The merged array.
473    *
474    * @see http://php.net/manual/en/function.array-merge-recursive.php#92195
475    */
476   protected function arrayMergeRecursiveDistinct(array $array1, array $array2) {
477     $merged = $array1;
478
479     foreach ($array2 as $key => &$value) {
480       if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
481         $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value);
482       }
483       else {
484         $merged[$key] = $value;
485       }
486     }
487
488     return $merged;
489   }
490
491 }