diff --git a/res/schema.json b/res/schema.json index bf4fd0630..bc273453d 100644 --- a/res/schema.json +++ b/res/schema.json @@ -209,6 +209,10 @@ "stub": { "description": "The relative file path to the stub file, or the flag to use the default stub.", "type": ["boolean", "string", "null"] + }, + "timestamp": { + "description": "The time at which the PHAR timestamp will be set.", + "type": ["string", "null"] } } } diff --git a/src/Box.php b/src/Box.php index 8585f8b74..04581a4d0 100644 --- a/src/Box.php +++ b/src/Box.php @@ -369,11 +369,16 @@ public function extractTo(string $directory, bool $overwrite = false): void $this->phar->extractTo($directory, overwrite: $overwrite); } - public function signWithTimestamps( - DateTimeImmutable $timestamp, - SigningAlgorithm $signingAlgorithm - ): void - { + public function sign( + SigningAlgorithm $signingAlgorithm, + ?DateTimeImmutable $timestamp = null, + ): void { + if (null === $timestamp) { + $this->phar->setSignatureAlgorithm($signingAlgorithm->value); + + return; + } + $phar = $this->phar; $phar->__destruct(); unset($this->phar); diff --git a/src/Configuration/Configuration.php b/src/Configuration/Configuration.php index 49ac3c140..beeb29b2d 100644 --- a/src/Configuration/Configuration.php +++ b/src/Configuration/Configuration.php @@ -211,6 +211,7 @@ final class Configuration private const REPLACEMENTS_KEY = 'replacements'; private const SHEBANG_KEY = 'shebang'; private const STUB_KEY = 'stub'; + private const TIMESTAMP = 'timestamp'; private ?string $mainScriptPath; private ?string $mainScriptContents; @@ -344,6 +345,8 @@ public static function create(?string $file, stdClass $raw): self $replacements = self::retrieveReplacements($raw, $file, $basePath, $logger); + $timestamp = self::retrieveTimestamp($raw, $logger); + return new self( $file, $alias, @@ -376,6 +379,7 @@ public static function create(?string $file, stdClass $raw): self $stubPath, $isInterceptsFileFunctions, $isStubGenerated, + $timestamp, $checkRequirements, $logger->getWarnings(), $logger->getRecommendations(), @@ -383,38 +387,39 @@ public static function create(?string $file, stdClass $raw): self } /** - * @param string $basePath Utility to private the base path used and be able to retrieve a - * path relative to it (the base path) - * @param array $composerJson The first element is the path to the `composer.json` file as a - * string and the second element its decoded contents as an - * associative array. - * @param array $composerLock The first element is the path to the `composer.lock` file as a - * string and the second element its decoded contents as an - * associative array. - * @param SplFileInfo[] $files List of files - * @param SplFileInfo[] $binaryFiles List of binary files - * @param bool $dumpAutoload Whether the Composer autoloader should be dumped - * @param bool $excludeComposerFiles Whether the Composer files composer.json, composer.lock and - * installed.json should be removed from the PHAR - * @param CompressionAlgorithm $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants - * @param null|int $fileMode File mode in octal form - * @param string $mainScriptPath The main script file path - * @param string $mainScriptContents The processed content of the main script file - * @param MapFile $fileMapper Utility to map the files from outside and inside the PHAR - * @param mixed $metadata The PHAR Metadata - * @param bool $promptForPrivateKey If the user should be prompted for the private key passphrase - * @param array $processedReplacements The processed list of replacement placeholders and their values - * @param null|non-empty-string $shebang The shebang line - * @param SigningAlgorithm $signingAlgorithm The PHAR siging algorithm. See \Phar constants - * @param null|string $stubBannerContents The stub banner comment - * @param null|string $stubBannerPath The path to the stub banner comment file - * @param null|string $stubPath The PHAR stub file path - * @param bool $isInterceptFileFuncs Whether Phar::interceptFileFuncs() should be used - * @param bool $isStubGenerated Whether if the PHAR stub should be generated - * @param bool $checkRequirements Whether the PHAR will check the application requirements before - * running - * @param string[] $warnings - * @param string[] $recommendations + * @param string $basePath Utility to private the base path used and be able to retrieve a + * path relative to it (the base path) + * @param array $composerJson The first element is the path to the `composer.json` file as a + * string and the second element its decoded contents as an + * associative array. + * @param array $composerLock The first element is the path to the `composer.lock` file as a + * string and the second element its decoded contents as an + * associative array. + * @param SplFileInfo[] $files List of files + * @param SplFileInfo[] $binaryFiles List of binary files + * @param bool $dumpAutoload Whether the Composer autoloader should be dumped + * @param bool $excludeComposerFiles Whether the Composer files composer.json, composer.lock and + * installed.json should be removed from the PHAR + * @param CompressionAlgorithm $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants + * @param null|int $fileMode File mode in octal form + * @param string $mainScriptPath The main script file path + * @param string $mainScriptContents The processed content of the main script file + * @param MapFile $fileMapper Utility to map the files from outside and inside the PHAR + * @param mixed $metadata The PHAR Metadata + * @param bool $promptForPrivateKey If the user should be prompted for the private key passphrase + * @param array $processedReplacements The processed list of replacement placeholders and their values + * @param null|non-empty-string $shebang The shebang line + * @param SigningAlgorithm $signingAlgorithm The PHAR siging algorithm. See \Phar constants + * @param null|string $stubBannerContents The stub banner comment + * @param null|string $stubBannerPath The path to the stub banner comment file + * @param null|string $stubPath The PHAR stub file path + * @param bool $isInterceptFileFuncs Whether Phar::interceptFileFuncs() should be used + * @param bool $isStubGenerated Whether if the PHAR stub should be generated + * @param null|DateTimeImmutable $timestamp Timestamp at which the PHAR will be set to. + * @param bool $checkRequirements Whether the PHAR will check the application requirements before + * running + * @param string[] $warnings + * @param string[] $recommendations */ private function __construct( private ?string $file, @@ -448,6 +453,7 @@ private function __construct( private ?string $stubPath, private bool $isInterceptFileFuncs, private bool $isStubGenerated, + private ?DateTimeImmutable $timestamp, private bool $checkRequirements, private array $warnings, private array $recommendations, @@ -679,6 +685,11 @@ public function isStubGenerated(): bool return $this->isStubGenerated; } + public function getTimestamp(): ?DateTimeImmutable + { + return $this->timestamp; + } + /** * @return string[] */ @@ -2064,6 +2075,22 @@ private static function retrieveReplacements( return $replacements; } + private static function retrieveTimestamp( + stdClass $raw, + ConfigurationLogger $logger, + ): ?DateTimeImmutable { + self::checkIfDefaultValue($logger, $raw, self::TIMESTAMP); + + $timestamp = $raw->{self::TIMESTAMP} ?? null; + + return null === $timestamp + ? null + : new DateTimeImmutable( + $timestamp, + new DateTimeZone('UTC'), + ); + } + private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string { return self::retrievePlaceholder($raw, $logger, self::GIT_KEY); diff --git a/src/Console/Command/Compile.php b/src/Console/Command/Compile.php index 72e2b0a1e..6fdf18e70 100644 --- a/src/Console/Command/Compile.php +++ b/src/Console/Command/Compile.php @@ -39,13 +39,12 @@ use KevinGH\Box\Console\MessageRenderer; use KevinGH\Box\MapFile; use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\SigningAlgorithm; use KevinGH\Box\RequirementChecker\DecodedComposerJson; use KevinGH\Box\RequirementChecker\DecodedComposerLock; use KevinGH\Box\RequirementChecker\RequirementsDumper; use KevinGH\Box\StubGenerator; -use Phar; use RuntimeException; -use Seld\PharUtils\Timestamps; use stdClass; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\StringInput; @@ -73,7 +72,6 @@ use function putenv; use function Safe\getcwd; use function sprintf; -use function str_replace; use function var_export; use const KevinGH\Box\BOX_ALLOW_XDEBUG; use const PHP_EOL; @@ -248,7 +246,8 @@ private function createPhar( IO $io, bool $debug, ): Box { - $box = Box::create($config->getTmpOutputPath()); + $tmpOutputPath = $config->getTmpOutputPath(); + $box = Box::create($tmpOutputPath); $composerOrchestrator = new ComposerOrchestrator( ComposerProcessFactory::create( $config->getComposerBin(), @@ -293,13 +292,10 @@ private function createPhar( $logger, ); - self::correctTimestamp($box, $logger); - exit; + self::signPhar($config, $box, $tmpOutputPath, $io, $logger); - self::signPhar($config, $box, $config->getTmpOutputPath(), $io, $logger); - - if ($config->getTmpOutputPath() !== $config->getOutputPath()) { - FS::rename($config->getTmpOutputPath(), $config->getOutputPath()); + if ($tmpOutputPath !== $config->getOutputPath()) { + FS::rename($tmpOutputPath, $config->getOutputPath()); } return $box; @@ -748,20 +744,6 @@ private static function configureCompressionAlgorithm( } } - private static function correctTimestamp(Box $box, CompilerLogger $logger): void { - $timestamp = new DateTimeImmutable('2017-10-11 08:58:00+00:00'); - - $logger->log( - CompilerLogger::QUESTION_MARK_PREFIX, - sprintf( - 'Correcting the timestamp to "%s".', - $timestamp->format(DateTimeInterface::ATOM), - ), - ); - - $box->signWithTimestamps($timestamp); - } - private static function signPhar( Configuration $config, Box $box, @@ -775,19 +757,57 @@ private static function signPhar( $key = $config->getPrivateKeyPath(); if (null === $key) { - $box->sign($config->getSigningAlgorithm()); + self::signPharWithoutPrivateKey( + $box, + $config->getSigningAlgorithm(), + $config->getTimestamp(), + $logger, + ); + } else { + self::signPharWithPrivateKey( + $box, + $key, + $config->getPrivateKeyPassphrase(), + $config->promptForPrivateKey(), + $io, + $logger, + ); + } + } - return; + private static function signPharWithoutPrivateKey( + Box $box, + SigningAlgorithm $signingAlgorithm, + ?DateTimeImmutable $timestamp, + CompilerLogger $logger, + ): void { + if (null !== $timestamp) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Correcting the timestamp to "%s".', + $timestamp->format(DateTimeInterface::ATOM), + ), + ); } + $box->sign($signingAlgorithm, $timestamp); + } + + private static function signPharWithPrivateKey( + Box $box, + string $key, + ?string $passphrase, + bool $prompt, + IO $io, + CompilerLogger $logger, + ): void { $logger->log( CompilerLogger::QUESTION_MARK_PREFIX, 'Signing using a private key', ); - $passphrase = $config->getPrivateKeyPassphrase(); - - if ($config->promptForPrivateKey()) { + if ($prompt) { if (false === $io->isInteractive()) { throw new RuntimeException( sprintf(