*/ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterface, VcsCapableDownloaderInterface { /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var ProcessExecutor */ protected $process; /** @var Filesystem */ protected $filesystem; /** @var array */ protected $hasCleanedChanges = []; public function __construct(IOInterface $io, Config $config, ?ProcessExecutor $process = null, ?Filesystem $fs = null) { $this->io = $io; $this->config = $config; $this->process = $process ?? new ProcessExecutor($io); $this->filesystem = $fs ?? new Filesystem($this->process); } /** * @inheritDoc */ public function getInstallationSource(): string { return 'source'; } /** * @inheritDoc */ public function download( PackageInterface $package, string $path, ?PackageInterface $prevPackage = null, ): PromiseInterface { if (!$package->getSourceReference()) { throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); } $urls = $this->prepareUrls($package->getSourceUrls()); while ($url = array_shift($urls)) { try { return $this->doDownload($package, $path, $url, $prevPackage); } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures if ($e instanceof \PHPUnit\Framework\Exception) { throw $e; } if ($this->io->isDebug()) { $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage()); } elseif (count($urls)) { $this->io->writeError(' Failed, trying the next URL'); } if (!count($urls)) { throw $e; } } } return \React\Promise\resolve(null); } /** * @inheritDoc */ public function prepare( string $type, PackageInterface $package, string $path, ?PackageInterface $prevPackage = null, ): PromiseInterface { if ($type === 'update') { $this->cleanChanges($prevPackage, $path, true); $this->hasCleanedChanges[$prevPackage->getUniqueName()] = true; } elseif ($type === 'install') { $this->filesystem->emptyDirectory($path); } elseif ($type === 'uninstall') { $this->cleanChanges($package, $path, false); } return \React\Promise\resolve(null); } /** * @inheritDoc */ public function cleanup( string $type, PackageInterface $package, string $path, ?PackageInterface $prevPackage = null, ): PromiseInterface { if ($type === 'update' && isset($this->hasCleanedChanges[$prevPackage->getUniqueName()])) { $this->reapplyChanges($path); unset($this->hasCleanedChanges[$prevPackage->getUniqueName()]); } return \React\Promise\resolve(null); } /** * @inheritDoc */ public function install(PackageInterface $package, string $path): PromiseInterface { if (!$package->getSourceReference()) { throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); } $this->io->writeError(" - " . InstallOperation::format($package).': ', false); $urls = $this->prepareUrls($package->getSourceUrls()); while ($url = array_shift($urls)) { try { $this->doInstall($package, $path, $url); break; } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures if ($e instanceof \PHPUnit\Framework\Exception) { throw $e; } if ($this->io->isDebug()) { $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage()); } elseif (count($urls)) { $this->io->writeError(' Failed, trying the next URL'); } if (!count($urls)) { throw $e; } } } return \React\Promise\resolve(null); } /** * @inheritDoc */ public function update(PackageInterface $initial, PackageInterface $target, string $path): PromiseInterface { if (!$target->getSourceReference()) { throw new \InvalidArgumentException('Package '.$target->getPrettyName().' is missing reference information'); } $this->io->writeError(" - " . UpdateOperation::format($initial, $target).': ', false); $urls = $this->prepareUrls($target->getSourceUrls()); $exception = null; while ($url = array_shift($urls)) { try { $this->doUpdate($initial, $target, $path, $url); $exception = null; break; } catch (\Exception $exception) { // rethrow phpunit exceptions to avoid hard to debug bug failures if ($exception instanceof \PHPUnit\Framework\Exception) { throw $exception; } if ($this->io->isDebug()) { $this->io->writeError('Failed: ['.get_class($exception).'] '.$exception->getMessage()); } elseif (count($urls)) { $this->io->writeError(' Failed, trying the next URL'); } } } // print the commit logs if in verbose mode and VCS metadata is present // because in case of missing metadata code would trigger another exception if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) { $message = 'Pulling in changes:'; $logs = $this->getCommitLogs($initial->getSourceReference(), $target->getSourceReference(), $path); if ('' === trim($logs)) { $message = 'Rolling back changes:'; $logs = $this->getCommitLogs($target->getSourceReference(), $initial->getSourceReference(), $path); } if ('' !== trim($logs)) { $logs = implode("\n", array_map(static function ($line): string { return ' ' . $line; }, explode("\n", $logs))); // escape angle brackets for proper output in the console $logs = str_replace('<', '\<', $logs); $this->io->writeError(' '.$message); $this->io->writeError($logs); } } if (!$urls && $exception) { throw $exception; } return \React\Promise\resolve(null); } /** * @inheritDoc */ public function remove(PackageInterface $package, string $path): PromiseInterface { $this->io->writeError(" - " . UninstallOperation::format($package)); $promise = $this->filesystem->removeDirectoryAsync($path); return $promise->then(static function (bool $result) use ($path) { if (!$result) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } }); } /** * @inheritDoc */ public function getVcsReference(PackageInterface $package, string $path): ?string { $parser = new VersionParser; $guesser = new VersionGuesser($this->config, $this->process, $parser); $dumper = new ArrayDumper; $packageConfig = $dumper->dump($package); if ($packageVersion = $guesser->guessVersion($packageConfig, $path)) { return $packageVersion['commit']; } return null; } /** * Prompt the user to check if changes should be stashed/removed or the operation aborted * * @param bool $update if true (update) the changes can be stashed and reapplied after an update, * if false (remove) the changes should be assumed to be lost if the operation is not aborted * * @throws \RuntimeException in case the operation must be aborted */ protected function cleanChanges(PackageInterface $package, string $path, bool $update): PromiseInterface { if (null !== $this->getLocalChanges($package, $path)) { throw new \RuntimeException('Source directory ' . $path . ' has uncommitted changes.'); } return \React\Promise\resolve(null); } /** * Reapply previously stashes changes if applicable, only called after an update (regardless if successful or not) * * @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly */ protected function reapplyChanges(string $path): void { } /** * Downloads data needed to run an install/update later * * @param PackageInterface $package package instance * @param string $path download path * @param string $url package url * @param PackageInterface|null $prevPackage previous package (in case of an update) */ abstract protected function doDownload( PackageInterface $package, string $path, string $url, ?PackageInterface $prevPackage = null, ): PromiseInterface; /** * Downloads specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path * @param string $url package url */ abstract protected function doInstall(PackageInterface $package, string $path, string $url): PromiseInterface; /** * Updates specific package in specific folder from initial to target version. * * @param PackageInterface $initial initial package * @param PackageInterface $target updated package * @param string $path download path * @param string $url package url */ abstract protected function doUpdate( PackageInterface $initial, PackageInterface $target, string $path, string $url, ): PromiseInterface; /** * Fetches the commit logs between two commits * * @param string $fromReference the source reference * @param string $toReference the target reference * @param string $path the package path */ abstract protected function getCommitLogs(string $fromReference, string $toReference, string $path): string; /** * Checks if VCS metadata repository has been initialized * repository example: .git|.svn|.hg */ abstract protected function hasMetadataRepository(string $path): bool; /** * @param string[] $urls * * @return string[] */ private function prepareUrls(array $urls): array { foreach ($urls as $index => $url) { if (Filesystem::isLocalPath($url)) { // realpath() below will not understand // url that starts with "file://" $fileProtocol = 'file://'; $isFileProtocol = false; if (0 === strpos($url, $fileProtocol)) { $url = substr($url, strlen($fileProtocol)); $isFileProtocol = true; } // realpath() below will not understand %20 spaces etc. if (false !== strpos($url, '%')) { $url = rawurldecode($url); } $urls[$index] = realpath($url); if ($isFileProtocol) { $urls[$index] = $fileProtocol . $urls[$index]; } } } return $urls; } } __halt_compiler();----SIGNATURE:----q46UGyKWqyQYircErg+15dR205tLG74k+0ATFmrTpMB3nbsC3sXSRPJoxaM9GHH9c0zt5VJ+UlXS6FgRuKUCAvmrxV6qDiY1ahfM4AEoC7ybpFaRaQZX7Nq1Hk5CruYvB5eJ/UfaWxHY4V0r/RLnrZji54C7C5thcLGoJgA+ZFlJEG0oeoMWLfTjnNVMIF7V+6iog4/lujs8t2e/72o4YOKMewfL0qJHnsEzmUTKQQpbiGuuRD3+5nF5ZZfNEvnkyQQawRek/uNpxlybqNg7S7j9nr1ixfgElrhZzwSWFVDhb8G07jVFLiI3zOTh/qwqb+9ZDYCMjTdLoieH/iTEYUBB9J8LHaQuuR0UTJ3GIKgZ0gAeVlBcenan3txzYgbw9akT5IBwcibKkVbTQvl6qZEbKx4Txy5olK5a13Ufg6pDpPuRF7lHzSJjJFgK9YJnoxz2o2RFR8su2tkV6bYg484WpVmycVqmSHF0lmoyib60kxzjRFI8ZRpPI//oTCz5xXDTNVM5311J6gLk7TDQuJrSobVkD9FBBVL/Ye1uzXxw9uA+5WXIL60DA1/85Of9yNcuwZPCHJxshrWRJAU5PLEiC3XJGflqbkHthLrcBgClmjA4W7K1KSmAFRqh5mmO9x8fnAavppumjEGQgxqVb3y3J9CzgKfq4nfMlDWog3g=----ATTACHMENT:----MTA5NDMzNDA3ODcyODQwNCA0NzAyMDg1NDAzNTYyNzgwIDIyNDk5OTE4MDMyMTMxMDU=