diff --git a/LICENSE b/LICENSE index 58bf89c..6c314c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,30 @@ -Copyright ComposerDistributor-Team +Phar.io - Composer Distributor -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +Copyright (c) 2020 Arne Blankerts and contributors +All rights reserved. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Arne Blankerts nor the names of contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/ConfiguredMediator.php b/src/ConfiguredMediator.php index e998bfd..e8819e3 100644 --- a/src/ConfiguredMediator.php +++ b/src/ConfiguredMediator.php @@ -7,9 +7,13 @@ use Composer\Composer; use Composer\Installer\PackageEvent; use Composer\IO\IOInterface; +use Exception; +use GnuPG; use PharIo\ComposerDistributor\Config\Config; use PharIo\ComposerDistributor\Config\Loader; use PharIo\ComposerDistributor\Service\Installer; +use PharIo\FileSystem\Directory; +use PharIo\GnuPG\Factory; use SplFileInfo; abstract class ConfiguredMediator extends PluginBase @@ -30,23 +34,49 @@ abstract protected function getDistributorConfig(): string; public function uninstall(Composer $composer, IOInterface $io) { - $this->composer = $composer; - $this->io = $io; + parent::uninstall($composer, $io); $this->removePhars(); } public function installOrUpdateFunction(PackageEvent $event): void { + $gnuPG = $this->createGnuPG(); + // we do not want to crash if no GnuPG was found + // but display a noticeable warning to the user + if ($gnuPG === null) { + $this->getIO()->write( + PHP_EOL . + ' WARNING' . PHP_EOL . + ' No GPG installation found! Use installed PHARs with care. ' . PHP_EOL . + ' Consider installing GnuPG to verify PHAR authenticity.' . PHP_EOL . + ' If you need help installing GnuPG visit http://phar.io/install-gnupg' . PHP_EOL + ); + } + $installer = $this->createInstallerFromConfig($this->config, $event); - $installer->install($this->config->phars()); + $installer->install( + $this->config->phars(), + $this->config->keyDirectory() ? $this->createKeyDirectory($this->config) : null, + $gnuPG + ); + } + + public function createGnuPG(): ?GnuPG + { + $factory = new Factory(); + try { + $gnuPG = $factory->createGnuPG(new Directory(sys_get_temp_dir())); + } catch (Exception $e) { + $gnuPG = null; + } + return $gnuPG; } private function createInstallerFromConfig(Config $config, PackageEvent $event): Installer { return new Installer( $config->package(), - $config->keyDirectory() ? $this->createKeyDirectory($config) : null, - $this->io, + $this->getIO(), $event ); } @@ -68,7 +98,7 @@ private function createKeyDirectory(Config $config): KeyDirectory private function removePhars(): void { - $binDir = $this->composer->getConfig()->get('bin-dir'); + $binDir = $this->getComposer()->getConfig()->get('bin-dir'); foreach ($this->config->phars()->getList() as $phar) { $this->deleteFile($phar, $binDir); @@ -81,12 +111,12 @@ private function deleteFile(File $phar, string $binDir): void if (is_file($pharLocation)) { if (!is_writable($pharLocation)) { - $this->io->write( + $this->getIO()->write( sprintf(' - Can not remove phar \'%1$s\' (insufficient permissions)', $phar->pharName()) ); return; } - $this->io->write(sprintf(' - Removing phar \'%1$s\'', $phar->pharName())); + $this->getIO()->write(sprintf(' - Removing phar %1$s', $phar->pharName())); unlink($pharLocation); } } diff --git a/src/PluginBase.php b/src/PluginBase.php index c5cda4b..05bd366 100644 --- a/src/PluginBase.php +++ b/src/PluginBase.php @@ -11,30 +11,33 @@ use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; use PharIo\ComposerDistributor\Service\Installer; -use SplFileInfo; abstract class PluginBase implements PluginInterface, EventSubscriberInterface { - protected $composer; + /** @var \Composer\Composer */ + private $composer; - protected $io; + /** @var \Composer\IO\IOInterface */ + private $io; + + abstract public function installOrUpdateFunction(PackageEvent $event) : void; public function activate(Composer $composer, IOInterface $io) { $this->composer = $composer; - $this->io = $io; + $this->io = $io; } public function deactivate(Composer $composer, IOInterface $io) { $this->composer = $composer; - $this->io = $io; + $this->io = $io; } public function uninstall(Composer $composer, IOInterface $io) { $this->composer = $composer; - $this->io = $io; + $this->io = $io; } public static function getSubscribedEvents() @@ -43,21 +46,34 @@ public static function getSubscribedEvents() PackageEvents::POST_PACKAGE_INSTALL => [ ['installOrUpdateFunction', 0], ], - PackageEvents::POST_PACKAGE_UPDATE => [ + PackageEvents::POST_PACKAGE_UPDATE => [ ['installOrUpdateFunction', 0], ], ]; } - public function createInstaller(string $pluginName, string $keyDirectory, PackageEvent $event) : Installer + public function createInstaller(string $pluginName, PackageEvent $event) : Installer { return new Installer( $pluginName, - new KeyDirectory(new SplFileInfo($keyDirectory)), $this->io, $event ); } - abstract public function installOrUpdateFunction(PackageEvent $event) : void; + protected function getIO(): IOInterface + { + if (!$this->io) { + throw new \RuntimeException('IO not set'); + } + return $this->io; + } + + protected function getComposer(): Composer + { + if (!$this->composer) { + throw new \RuntimeException('Composer not set'); + } + return $this->composer; + } } diff --git a/src/Service/Installer.php b/src/Service/Installer.php index 19adfb6..33b1f10 100644 --- a/src/Service/Installer.php +++ b/src/Service/Installer.php @@ -6,8 +6,7 @@ use Composer\Installer\PackageEvent; use Composer\IO\IOInterface; -use PharIo\FileSystem\Directory; -use PharIo\GnuPG\Factory; +use GnuPG; use PharIo\ComposerDistributor\SomebodyElsesProblem; use PharIo\ComposerDistributor\File; use PharIo\ComposerDistributor\FileList; @@ -37,15 +36,17 @@ final class Installer /** @var \Composer\Installer\PackageEvent */ private $event; - public function __construct(string $name, ?KeyDirectory $keys, IOInterface $io, PackageEvent $event) + /** @var \GnuPG|null */ + private $gpg; + + public function __construct(string $name, IOInterface $io, PackageEvent $event) { $this->name = $name; - $this->keys = $keys; $this->io = $io; $this->event = $event; } - public function install(FileList $fileList) : void + public function install(FileList $fileList, ?KeyDirectory $keys, ?GnuPG $gnuPG) : void { try { $packageVersion = PackageVersion::fromPackageEvent($this->event, $this->name); @@ -53,45 +54,57 @@ public function install(FileList $fileList) : void $this->io->write($e->getMessage()); return; } - + $this->keys = $keys; + $this->gpg = $gnuPG; $versionReplacer = new VersionConstraintReplacer($packageVersion); + $binDir = $this->event->getComposer()->getConfig()->get('bin-dir'); + + if (!file_exists($binDir)) { + mkdir($binDir, 0777, true); + } foreach ($fileList->getList() as $file) { $this->io->write(sprintf( - ' - Downloading artifact in version %2$s from %1$s', - $versionReplacer->replace($file->pharUrl()->toString()), - $packageVersion->fullVersion() + ' - Downloading artifact from %1$s', + $versionReplacer->replace($file->pharUrl()->toString()) )); - $pharLocation = $this->downloadPhar($versionReplacer, $file); + $downloadLocation = $this->downloadAndVerify($versionReplacer, $file); + $installLocation = new SplFileInfo($binDir . DIRECTORY_SEPARATOR . $file->pharName()); + + rename($downloadLocation->getPathname(), $installLocation->getPathname()); + chmod($installLocation->getPathname(), 0755); + } + } + + private function downloadAndVerify(VersionConstraintReplacer $versionReplacer, File $file): SplFileInfo + { + $pharLocation = $this->downloadPhar($versionReplacer, $file); - if (!$file->signatureUrl()) { - $this->io->write(' - No digital signature found! Use this file with care!'); - continue; - } + if (!$file->signatureUrl()) { + $this->io->write(' - No digital signature found! Use this file with care!'); + return $pharLocation; + } - $signatureLocation = $this->downloadSignature($versionReplacer, $file); - $this->verifyPharWithSignature($pharLocation, $signatureLocation); - $this->io->write(' - PHAR signature successfully verified'); - unlink($signatureLocation->getPathname()); + if ($this->gpg === null) { + $this->io->write(' - No GnuPG found to verify signature! Use this file with care!'); + return $pharLocation; } + + $signatureLocation = $this->downloadSignature($versionReplacer, $file); + $this->verifyPharWithSignature($pharLocation, $signatureLocation); + $this->io->write(' - PHAR signature successfully verified'); + unlink($signatureLocation->getPathname()); + + return $pharLocation; } private function downloadPhar(VersionConstraintReplacer $versionReplacer, File $file): SplFileInfo { - $binDir = $this->event->getComposer()->getConfig()->get('bin-dir'); - $download = new Download(Url::fromString( - $versionReplacer->replace($file->pharUrl()->toString()) - )); - $pharLocation = new SplFileInfo( - $binDir . DIRECTORY_SEPARATOR . $file->pharName() - ); + $download = new Download(Url::fromString($versionReplacer->replace($file->pharUrl()->toString()))); + $pharLocation = new SplFileInfo(sys_get_temp_dir() . '/' . $file->pharName()); - if (! file_exists($binDir)) { - mkdir($binDir, 0777, true); - } $download->toLocation($pharLocation); - chmod($pharLocation->getRealPath(), 0755); return $pharLocation; } @@ -109,14 +122,11 @@ private function downloadSignature(VersionConstraintReplacer $versionReplacer, F private function verifyPharWithSignature(SplFileInfo $pharLocation, SplFileInfo $signatureLocation): void { - if (null === $this->keys) { + if ($this->keys === null) { throw new RuntimeException('No keys to verify the signature'); } - $factory = new Factory(); - $verify = new Verify( - $this->keys, - $factory->createGnuPG(new Directory(sys_get_temp_dir())) - ); + + $verify = new Verify($this->keys, $this->gpg); if (!$verify->fileWithSignature($pharLocation, $signatureLocation)) { throw new RuntimeException('Signature Verification failed');