diff --git a/3rdparty b/3rdparty index cddfc39be9..63d763a37f 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit cddfc39be9e5841421ffb954f919afc2ff6a9809 +Subproject commit 63d763a37f2a1f26a312a24e0286c4281eb3cc86 diff --git a/lib/Handler/SignEngine/JSignPdfHandler.php b/lib/Handler/SignEngine/JSignPdfHandler.php index 2dc7ddc143..e197ee1bcb 100644 --- a/lib/Handler/SignEngine/JSignPdfHandler.php +++ b/lib/Handler/SignEngine/JSignPdfHandler.php @@ -15,7 +15,6 @@ use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Helper\JavaHelper; use OCA\Libresign\Service\DocMdp\ConfigService as DocMdpConfigService; -use OCA\Libresign\Service\Install\InstallService; use OCA\Libresign\Service\Policy\PolicyService; use OCA\Libresign\Service\Policy\Provider\SignatureHashAlgorithm\SignatureHashAlgorithmPolicy; use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicyValue; @@ -82,34 +81,29 @@ public function getJSignParam(): JSignParam { if (!is_writable($tempPath)) { throw new \Exception('The path ' . $tempPath . ' is not writtable. Fix this or change the LibreSign app setting jsignpdf_temp_path to a writtable path'); } - $jSignPdfJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path', '/opt/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar'); - if (!file_exists($jSignPdfJarPath)) { - throw new \Exception('Invalid JSignPdf jar path. Run occ libresign:install --jsignpdf'); + $jSignPdfPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); + if (!is_dir($jSignPdfPath)) { + throw new \Exception('Invalid JSignPdf path. Run occ libresign:install --jsignpdf'); } + $home = $this->getHome(); $this->jSignParam = (new JSignParam()) ->setTempPath($tempPath) ->setIsUseJavaInstalled(empty($javaPath)) ->setJavaDownloadUrl('') ->setJSignPdfDownloadUrl('') - ->setjSignPdfJarPath($jSignPdfJarPath); + ->setJSignPdfPath($jSignPdfPath) + ->setJavaOptions(['-Duser.home=' . $home]) + ->setEnvironmentVariables(['JSIGNPDF_HOME' => $home]); if (!empty($javaPath)) { if (!file_exists($javaPath)) { throw new \Exception('Invalid Java binary. Run occ libresign:install --java'); } - $this->jSignParam->setJavaPath( - $this->getEnvironments() - . $javaPath - . ' -Duser.home=' . escapeshellarg($this->getHome()) . ' ' - ); + $this->jSignParam->setJavaPath($javaPath); } } return $this->jSignParam; } - private function getEnvironments(): string { - return 'JSIGNPDF_HOME=' . escapeshellarg($this->getHome()) . ' '; - } - /** * It's a workaround to create the folder structure that JSignPdf needs. Without * this, the JSignPdf will return the follow message to all commands: @@ -264,36 +258,28 @@ public function getSignedContent(): string { $normalizedPdf = $this->normalizePdfVersion($this->getInputFile()->getContent()); $hashAlgorithm = $this->getHashAlgorithm($normalizedPdf); $param = $this->getJSignParam(); - - $tsaParams = $this->listParamsToString($this->getTsaParameters()); - - $visibleElements = $this->getVisibleElements(); - $certParams = ''; - $certificationLevel = $this->getCertificationLevel(); - if ($certificationLevel !== null && !$visibleElements && !$this->hasExistingSignatures($normalizedPdf)) { - $certParams = ' -cl ' . $certificationLevel; - } - - $param->setJSignParameters( - $param->getJSignParameters() - . $certParams - . $tsaParams - ); $param->setCertificate($this->getCertificate()) ->setPdf($normalizedPdf) ->setPassword($this->getPassword()); + $parameters = []; + $certificationLevel = $this->getCertificationLevel(); + if ($certificationLevel !== null && !$this->getVisibleElements() && !$this->hasExistingSignatures($normalizedPdf)) { + $parameters['-cl'] = $certificationLevel; + } + $parameters += $this->getTsaParameters(); + $param->addJSignParameters($parameters); + $tsaPassword = $this->getTsaPassword(); + if ($tsaPassword !== '') { + $param->setTsaPassword($tsaPassword); + } + $signed = $this->signUsingVisibleElements($normalizedPdf, $hashAlgorithm); if ($signed) { return $signed; } - $param->setJSignParameters( - $param->getJSignParameters() - . $this->listParamsToString([ - '--hash-algorithm' => $hashAlgorithm, - ]) - ); + $param->addJSignParameters(['--hash-algorithm' => $hashAlgorithm]); $jSignPdf = $this->getJSignPdf(); $jSignPdf->setParam($param); return $this->signWrapper($jSignPdf); @@ -313,7 +299,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg ]; // When l2-text is empty, add hash-algorithm at the beginning - if ($params['--l2-text'] === '""') { + if ($params['--l2-text'] === '') { $params = [ '--hash-algorithm' => $hashAlgorithm, '--l2-text' => $params['--l2-text'], @@ -322,7 +308,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg } $fontSize = $this->parseSignatureText()['templateFontSize']; - if ($fontSize === SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE || !$fontSize || $params['--l2-text'] === '""') { + if ($fontSize === SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE || !$fontSize || $params['--l2-text'] === '') { $fontSize = 0; } @@ -335,11 +321,9 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg $certificationLevel = $this->getCertificationLevel(); $applyCertification = $certificationLevel !== null && !$this->hasExistingSignatures($normalizedPdf); - $certParams = $applyCertification ? ' -cl ' . $certificationLevel : ''; $elementIndex = 0; - $param = $this->getJSignParam(); - $originalParam = clone $param; + $originalParam = $this->getJSignParam(); foreach ($visibleElements as $element) { $elementIndex++; @@ -374,7 +358,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg } elseif ($signatureImagePath) { $params['--bg-path'] = $signatureImagePath; } - } elseif ($params['--l2-text'] === '""') { + } elseif ($params['--l2-text'] === '') { if ($backgroundPathForElement && $signatureImagePath) { $params['--bg-path'] = $this->mergeBackgroundWithSignature( $backgroundPathForElement, @@ -409,16 +393,15 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg } // Only add hash-algorithm at the end if l2-text is not empty - if ($params['--l2-text'] !== '""') { + if ($params['--l2-text'] !== '') { $params['--hash-algorithm'] = $hashAlgorithm; } - $elementCertParams = ($applyCertification && $elementIndex === 1) ? $certParams : ''; - $param->setJSignParameters( - $originalParam->getJSignParameters() - . $elementCertParams - . $this->listParamsToString($params) - ); + $param = clone $originalParam; + if ($applyCertification && $elementIndex === 1) { + $param->addJSignParameters(['-cl' => $certificationLevel]); + } + $param->addJSignParameters($this->toJSignParameters($params)); $param->setPdf($normalizedPdf); $jSignPdf->setParam($param); $signed = $this->signWrapper($jSignPdf); @@ -625,61 +608,63 @@ private function shouldUseJSignTimestampPlaceholder(string $template): bool { public function getSignatureText(): string { $renderMode = $this->signatureTextService->getRenderMode(); if ($renderMode !== SignerElementsService::RENDER_MODE_GRAPHIC_ONLY) { - $data = $this->parseSignatureText(); - $signatureText = '"' . str_replace( - ['"', '$'], - ['\"', '\$'], - $data['parsed'] - ) . '"'; - } else { - $signatureText = '""'; + return $this->parseSignatureText()['parsed']; } - - return $signatureText; + return ''; } - private function listParamsToString(array $params): string { - $paramString = ''; - foreach ($params as $flag => $value) { - $paramString .= ' ' . $flag; - if ($value !== null && $value !== '') { - $paramString .= ' ' . $value; + /** + * Options with a null value are flags. Every other value reaches the + * wrapper as a string; the wrapper escapes it for the shell. + * + * @param array $params + * @return array + */ + private function toJSignParameters(array $params): array { + $parameters = []; + foreach ($params as $option => $value) { + if ($value === null) { + $parameters[] = $option; + continue; } + $parameters[$option] = (string)$value; } - return $paramString; + return $parameters; } + /** + * @return array + */ private function getTsaParameters(): array { $tsaSettings = $this->getTsaSettings(); - $tsaUrl = $tsaSettings['url']; - if (empty($tsaUrl)) { + if (empty($tsaSettings['url'])) { return []; } - $params = [ - '--tsa-server-url' => $tsaUrl, - '--tsa-policy-oid' => $tsaSettings['policy_oid'], - ]; - - if (!$params['--tsa-policy-oid']) { - unset($params['--tsa-policy-oid']); + $params = ['--tsa-server-url' => $tsaSettings['url']]; + if ($tsaSettings['policy_oid']) { + $params['--tsa-policy-oid'] = $tsaSettings['policy_oid']; } - - $tsaAuthType = $tsaSettings['auth_type']; - if ($tsaAuthType === 'basic') { - $tsaUsername = $tsaSettings['username']; - $tsaPassword = $this->appConfig->getValueString(Application::APP_ID, TsaPolicy::PASSWORD_APP_CONFIG_KEY, ''); - - if (!empty($tsaUsername) && !empty($tsaPassword)) { - $params['--tsa-authentication'] = 'PASSWORD'; - $params['--tsa-user'] = $tsaUsername; - $params['--tsa-password'] = $tsaPassword; - } + if ($this->getTsaPassword() !== '') { + $params['--tsa-authentication'] = 'PASSWORD'; + $params['--tsa-user'] = $tsaSettings['username']; } return $params; } + /** + * The TSA password never goes to the command line: the wrapper writes it + * to the stdin of JSignPdf. + */ + private function getTsaPassword(): string { + $tsaSettings = $this->getTsaSettings(); + if (empty($tsaSettings['url']) || $tsaSettings['auth_type'] !== 'basic' || empty($tsaSettings['username'])) { + return ''; + } + return $this->appConfig->getValueString(Application::APP_ID, TsaPolicy::PASSWORD_APP_CONFIG_KEY, ''); + } + /** * @return array{url: string, policy_oid: string, auth_type: string, username: string} */ diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index f24ed55350..e1f5623dd4 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -50,8 +50,7 @@ class InstallService { private const string JAVA_URL_PATH_NAME = '21.0.8+9'; public const PDFTK_VERSION = '3.3.3'; /** @todo When update, verify the hash **/ private const string PDFTK_HASH = '59a28bed53b428595d165d52988bf4cf'; - public const JSIGNPDF_VERSION = '2.3.0'; /** @todo When update, verify the hash **/ - private const string JSIGNPDF_HASH = 'd239658ea50a39eb35169d8392feaffb'; + public const JSIGNPDF_VERSION = JSignPdfRelease::VERSION; public const CFSSL_VERSION = '1.6.5'; private const string PROCESS_SOURCE = 'install'; @@ -477,36 +476,34 @@ public function installJSignPdf(?bool $async = false): void { if ($this->isDownloadedFilesOk()) { // The binaries files could exists but not saved at database - $fullPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + $fullPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); if (!$fullPath) { $folder = $this->getFolder($this->resource); - $extractDir = $this->getInternalPathOfFolder($folder); - $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar'; - $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath); + $fullPath = JSignPdfRelease::installPath($this->getInternalPathOfFolder($folder)); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', $fullPath); } $this->saveJsignPdfHome(); - if (str_contains($fullPath, InstallService::JSIGNPDF_VERSION)) { + if (str_contains($fullPath, InstallService::JSIGNPDF_VERSION) && is_dir($fullPath)) { return; } } $folder = $this->getFolder($this->resource); - $compressedFileName = 'jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip'; + $compressedFileName = JSignPdfRelease::archiveName(); try { $compressedFile = $folder->getFile($compressedFileName); } catch (\Throwable) { $compressedFile = $folder->newFile($compressedFileName); } $compressedInternalFileName = $this->getInternalPathOfFile($compressedFile); - $url = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_' . str_replace('.', '_', InstallService::JSIGNPDF_VERSION) . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '.zip'; - - $this->download($url, 'JSignPdf', $compressedInternalFileName, self::JSIGNPDF_HASH); + $hash = $this->getHash($compressedFileName, JSignPdfRelease::checksumUrl()); + $this->download(JSignPdfRelease::downloadUrl(), 'JSignPdf', $compressedInternalFileName, $hash, 'sha256'); $extractDir = $this->getInternalPathOfFolder($folder); $zip = new ZIP($extractDir . '/' . $compressedFileName); $zip->extract($extractDir); unlink($extractDir . '/' . $compressedFileName); - $fullPath = $extractDir . '/jsignpdf-' . InstallService::JSIGNPDF_VERSION . '/JSignPdf.jar'; - $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', $fullPath); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', JSignPdfRelease::installPath($extractDir)); + $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path'); $this->saveJsignPdfHome(); $this->writeAppSignature(); @@ -536,8 +533,9 @@ private function saveJsignPdfHome(): void { } public function uninstallJSignPdf(): void { - $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); - if (!$jsignpdJarPath) { + $jsignpdfPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path') + ?: $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + if (!$jsignpdfPath) { return; } $this->setResource('jsignpdf'); @@ -546,6 +544,7 @@ public function uninstallJSignPdf(): void { $folder->delete(); } catch (NotFoundException) { } + $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_path'); $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_jar_path'); $this->appConfig->deleteKey(Application::APP_ID, 'jsignpdf_home'); } diff --git a/lib/Service/Install/JSignPdfRelease.php b/lib/Service/Install/JSignPdfRelease.php new file mode 100644 index 0000000000..4929acd5dd --- /dev/null +++ b/lib/Service/Install/JSignPdfRelease.php @@ -0,0 +1,49 @@ +appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + $path = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); if (!$path) { // fallback try { @@ -212,7 +212,7 @@ public function getInstallPath(): string { throw new InvalidSignatureException('JSignPdf path not found at app config.'); } } - $installPath = substr($path, 0, strrpos($path, '/', -strlen('_/JSignPdf.jar'))); + $installPath = dirname($path); break; case 'pdftk': $path = $this->appConfig->getValueString(Application::APP_ID, 'pdftk_path'); diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 2a222deb4c..e27b077b85 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -10,6 +10,8 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ExtractedSignature; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\TimestampToken; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; @@ -103,14 +105,14 @@ public function validateFromString(string $pdfContent): array { /** * @param resource $resource - * @return list, certificateValidation: ValidationResult}> + * @return list, certificateValidation: ValidationResult, timestamp: TimestampToken|null}> */ protected function validateNativeFromResource($resource): array { return $this->validator->validateFromResource($resource); } /** - * @return list, certificateValidation: ValidationResult}> + * @return list, certificateValidation: ValidationResult, timestamp: TimestampToken|null}> */ protected function validateNativeFromString(string $pdfContent): array { return $this->validator->validateFromString($pdfContent); diff --git a/lib/SetupCheck/JSignPdfSetupCheck.php b/lib/SetupCheck/JSignPdfSetupCheck.php index 22dee5b717..873a51eb47 100644 --- a/lib/SetupCheck/JSignPdfSetupCheck.php +++ b/lib/SetupCheck/JSignPdfSetupCheck.php @@ -71,9 +71,9 @@ public function getCategory(): string { #[\Override] public function run(): SetupResult { $debugEnabled = $this->systemConfig->getSystemValueBool('debug', false); - $jsignpdfJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path'); + $jsignpdfPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_path'); - if (!$jsignpdfJarPath) { + if (!$jsignpdfPath) { return SetupResult::error( // TRANSLATORS Warning shown in Nextcloud administration overview when the optional JSignPdf signing backend is not found. $this->l10n->t('JSignPdf not found'), @@ -88,12 +88,12 @@ public function run(): SetupResult { return SetupResult::error($errorMsg, $tip); } - if (!file_exists($jsignpdfJarPath)) { + if (!is_dir($jsignpdfPath)) { return SetupResult::error( // TRANSLATORS JSignPdf is an optional external signing backend used by LibreSign. // LibreSign also supports other signing methods, including its native PHP signer. // %s is the configured JSignPdf path that could not be found. - $this->l10n->t('JSignPdf file not found: %s', [$jsignpdfJarPath]), + $this->l10n->t('JSignPdf path not found: %s', [$jsignpdfPath]), // TRANSLATORS Command to run into terminal using Nextcloud occ to configure LibreSign using CLI when the sysadmin want to do this by CLI. $this->l10n->t('Run %s', ['occ libresign:install --jsignpdf']) ); @@ -149,8 +149,8 @@ public function run(): SetupResult { // TRANSLATORS JSignPdf is an optional external signing backend. %s is the detected JSignPdf version. $this->l10n->t('JSignPdf version: %s', [$currentVersion]), - // TRANSLATORS JSignPdf is an optional external signing backend. %s is the configured or detected path to the JSignPdf executable/JAR file. - $this->l10n->t('JSignPdf path: %s', [$jsignpdfJarPath]), + // TRANSLATORS JSignPdf is an optional external signing backend. %s is the configured or detected directory where JSignPdf is installed. + $this->l10n->t('JSignPdf path: %s', [$jsignpdfPath]), ]; return SetupResult::success(implode("\n", $messages)); diff --git a/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php b/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php index 3e52ef5fb6..b9cbd48ede 100644 --- a/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php @@ -16,6 +16,7 @@ use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\SignEngine\JSignPdfHandler; use OCA\Libresign\Helper\JavaHelper; +use OCA\Libresign\Service\CaIdentifierService; use OCA\Libresign\Service\DocMdp\ConfigService as DocMdpConfigService; use OCA\Libresign\Service\Policy\Model\ResolvedPolicy; use OCA\Libresign\Service\Policy\PolicyService; @@ -24,6 +25,7 @@ use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicy; use OCA\Libresign\Service\Policy\Provider\SignatureText\SignatureTextPolicyValue; use OCA\Libresign\Service\Policy\Provider\Tsa\TsaPolicy; +use OCA\Libresign\Service\Policy\Provider\Tsa\TsaPolicyValue; use OCA\Libresign\Service\SignatureBackgroundService; use OCA\Libresign\Service\SignatureTextService; use OCA\Libresign\Service\SignerElementsService; @@ -60,6 +62,8 @@ public static function setUpBeforeClass(): void { try { $appConfig = self::getMockAppConfig(); $appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl'); + // The CRL distribution point of the root certificate needs a CA identifier. + \OCP\Server::get(CaIdentifierService::class)->generateCaId('openssl'); self::$certificateEngineFactory = \OCP\Server::get(CertificateEngineFactory::class); $certificateEngine = self::$certificateEngineFactory->getEngine(); $certificateEngine @@ -319,7 +323,7 @@ public function testSignAffectedParams( float $templateFontSize, string $pdfContent, ?string $hashAlgorithm, - string $params, + array $params, ):void { if (self::$certificateEngineFactory === null || empty(self::$certificateContent)) { $this->markTestSkipped('Certificate initialization failed'); @@ -328,7 +332,12 @@ public function testSignAffectedParams( $inputFile = $this->createMock(\OC\Files\Node\File::class); $inputFile->method('getContent') ->willReturn($pdfContent); + $paramsSeen = []; $mock = $this->createMock(JSignPDF::class); + $mock->method('setParam') + ->willReturnCallback(function (JSignParam $param) use (&$paramsSeen): void { + $paramsSeen[] = $param->getJSignParameters(); + }); $mock->method('sign')->willReturn('content'); $this->signatureBackgroundService->method('getSignatureBackgroundType')->willReturn( @@ -351,7 +360,7 @@ public function testSignAffectedParams( $this->persistHashAlgorithmPolicy($hashAlgorithm ?? ''); $this->appConfig->setValueString('libresign', 'java_path', __FILE__); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); $jSignPdfHandler = $this->getInstance(); $jSignPdfHandler->setVisibleElements($visibleElements); @@ -362,15 +371,28 @@ public function testSignAffectedParams( $jSignPdfHandler->setPassword('password'); $actual = $jSignPdfHandler->getSignedContent(); $this->assertEquals('content', $actual); - $jSignParam = $jSignPdfHandler->getJSignParam(); - $this->assertEquals('password', $jSignParam->getPassword()); - $paramsAsOptions = $jSignParam->getJSignParameters(); - $paramsAsOptions = preg_replace('/\\/\S+_merged.png/', 'merged.png', $paramsAsOptions); + $this->assertEquals('password', $jSignPdfHandler->getJSignParam()->getPassword()); + $this->assertCount(1, $paramsSeen); + $paramsAsOptions = preg_replace('/\\/\S+_merged.png/', 'merged.png', $paramsSeen[0]); $paramsAsOptions = preg_replace('/\\/\S+_text_image.png/', 'text_image.png', (string)$paramsAsOptions); $paramsAsOptions = preg_replace('/\\/\S+_background.png/', 'background.png', (string)$paramsAsOptions); $paramsAsOptions = preg_replace('/\\/\S+app-dark.png/', 'signature.png', (string)$paramsAsOptions); - $paramsAsOptions = preg_replace('/ --tsa-server-url\s+\S+/', '', (string)$paramsAsOptions); - $this->assertEquals($params, $paramsAsOptions); + $this->assertSame(self::expectedJSignParameters($params), $paramsAsOptions); + } + + /** + * What JSignParam::getJSignParameters() renders: the wrapper defaults + * followed by the given options, each option and value escaped for the + * shell, flags as bare escaped tokens. + */ + private static function expectedJSignParameters(array $params): string { + $tokens = []; + foreach (array_merge(['-a', '-kst' => 'PKCS12'], $params) as $option => $value) { + $tokens[] = is_string($option) + ? escapeshellarg($option) . ' ' . escapeshellarg($value) + : escapeshellarg($value); + } + return implode(' ', $tokens); } public static function providerSignAffectedParams(): array { @@ -385,7 +407,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 0, 'pdfContent' => '%PDF-1', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA1', + 'params' => ['--hash-algorithm' => 'SHA1'], ], 'page = 1 is default, do not will set the page' => [ 'visibleElements' => [self::getElement([ @@ -403,7 +425,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -llx 0 -lly 0 -urx 0 -ury 0 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-llx' => '0', '-lly' => '0', '-urx' => '0', '-ury' => '0', '--bg-path' => 'merged.png'] ], 'page != 1: will have pg; without template: l2-text empty' => [ 'visibleElements' => [self::getElement([ @@ -421,7 +443,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'] ], 'with template we have the l2-text' => [ 'visibleElements' => [self::getElement([ @@ -439,7 +461,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path background.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'background.png', '--hash-algorithm' => 'SHA256'] ], 'font size != default font size: emits --font-size' => [ 'visibleElements' => [self::getElement([ @@ -457,7 +479,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 11, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --font-size 11 --bg-path background.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--font-size' => '11', '--bg-path' => 'background.png', '--hash-algorithm' => 'SHA256'] ], 'background = deleted: bg-path = signature' => [ 'visibleElements' => [self::getElement([ @@ -475,7 +497,43 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path signature.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'signature.png', '--hash-algorithm' => 'SHA256'] + ], + 'template with shell special characters reaches the wrapper unescaped' => [ + 'visibleElements' => [self::getElement([ + 'page' => 2, + 'llx' => 10, + 'lly' => 20, + 'urx' => 30, + 'ury' => 40, + ], realpath(__DIR__ . '/../../../../../img/app-dark.png'))], + 'signatureWidth' => 20, + 'signatureHeight' => 20, + 'template' => 'a"b $c \'d e', + 'signatureBackgroundType' => 'deleted', + 'renderMode' => SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY, + 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, + 'pdfContent' => '%PDF-1.6', + 'hashAlgorithm' => '', + 'params' => ['--l2-text' => 'a"b $c \'d e', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'signature.png', '--hash-algorithm' => 'SHA256'], + ], + 'font size != default but no template: no --font-size' => [ + 'visibleElements' => [self::getElement([ + 'page' => 2, + 'llx' => 10, + 'lly' => 20, + 'urx' => 30, + 'ury' => 40, + ], realpath(__DIR__ . '/../../../../../img/app-dark.png'))], + 'signatureWidth' => 20, + 'signatureHeight' => 20, + 'template' => '', + 'signatureBackgroundType' => 'default', + 'renderMode' => SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY, + 'templateFontSize' => 11, + 'pdfContent' => '%PDF-1.6', + 'hashAlgorithm' => '', + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'], ], 'background and template, bg-path = background, img-path = signature' => [ 'visibleElements' => [self::getElement([ @@ -493,7 +551,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --bg-path background.png --img-path signature.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--bg-path' => 'background.png', '--img-path' => 'signature.png', '--hash-algorithm' => 'SHA256'] ], 'background and template, render mode equals to SIGNAME_AND_DESCRIPTION: bg-path = background, img-path = text_image' => [ 'visibleElements' => [self::getElement([ @@ -511,7 +569,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 1 -lly 100 -urx 351 -ury 200 --render-mode GRAPHIC_AND_DESCRIPTION --bg-path background.png --img-path text_image.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '1', '-lly' => '100', '-urx' => '351', '-ury' => '200', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--bg-path' => 'background.png', '--img-path' => 'text_image.png', '--hash-algorithm' => 'SHA256'] ], 'template without background; with signature image; render-mode: SIGNAME_AND_DESCRIPTION' => [ 'visibleElements' => [self::getElement([ @@ -529,7 +587,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --img-path text_image.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--img-path' => 'text_image.png', '--hash-algorithm' => 'SHA256'] ], 'template without background; without signature image; render-mode: SIGNAME_AND_DESCRIPTION' => [ 'visibleElements' => [self::getElement([ @@ -547,7 +605,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --img-path text_image.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--img-path' => 'text_image.png', '--hash-algorithm' => 'SHA256'] ], // Regression: background with GRAPHIC_AND_DESCRIPTION but NO user signature image. // Before the fix, mergeBackgroundWithSignature('...', '') crashed with new Imagick(''). @@ -568,7 +626,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --l2-text "aaaaa" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --render-mode GRAPHIC_AND_DESCRIPTION --bg-path background.png --hash-algorithm SHA256' + 'params' => ['--l2-text' => 'aaaaa', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--render-mode' => 'GRAPHIC_AND_DESCRIPTION', '--bg-path' => 'background.png', '--hash-algorithm' => 'SHA256'] ], 'background without template: bg-path = merged with signature, without img-path' => [ 'visibleElements' => [self::getElement([ @@ -586,7 +644,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'] ], 'regression: invalid stored dimensions should fallback to defaults and keep signing flow' => [ 'visibleElements' => [self::getElement([ @@ -604,7 +662,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 'pdfContent' => '%PDF-1.6', 'hashAlgorithm' => '', - 'params' => '-a -kst PKCS12 --hash-algorithm SHA256 --l2-text "" -V -pg 2 -llx 10 -lly 20 -urx 30 -ury 40 --bg-path merged.png' + 'params' => ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-pg' => '2', '-llx' => '10', '-lly' => '20', '-urx' => '30', '-ury' => '40', '--bg-path' => 'merged.png'] ], ]; } @@ -626,7 +684,7 @@ public function testDocMdpAppliedOnlyOnFirstVisibleElement(): void { $this->persistHashAlgorithmPolicy(''); $this->appConfig->setValueString('libresign', 'java_path', __FILE__); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); $paramsSeen = []; $mock = $this->createMock(JSignPDF::class); @@ -668,8 +726,8 @@ public function testDocMdpAppliedOnlyOnFirstVisibleElement(): void { $jSignPdfHandler->getSignedContent(); $this->assertCount(2, $paramsSeen); - $this->assertStringContainsString(' -cl ' . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, $paramsSeen[0]); - $this->assertStringNotContainsString(' -cl ' . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, $paramsSeen[1]); + $this->assertStringContainsString("'-cl' '" . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name . "'", $paramsSeen[0]); + $this->assertStringNotContainsString("'-cl' '" . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name . "'", $paramsSeen[1]); } public function testDocMdpSkippedWhenSignatureExists(): void { @@ -689,7 +747,7 @@ public function testDocMdpSkippedWhenSignatureExists(): void { $this->persistHashAlgorithmPolicy(''); $this->appConfig->setValueString('libresign', 'java_path', __FILE__); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); $paramsSeen = []; $mock = $this->createMock(JSignPDF::class); @@ -724,7 +782,7 @@ public function testDocMdpSkippedWhenSignatureExists(): void { $jSignPdfHandler->getSignedContent(); $this->assertCount(1, $paramsSeen); - $this->assertStringNotContainsString(' -cl ' . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, $paramsSeen[0]); + $this->assertStringNotContainsString("'-cl' '" . DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name . "'", $paramsSeen[0]); } #[DataProvider('providerSignatureDimensions')] @@ -823,44 +881,43 @@ private static function getElement(array $attributes = [], string $imagePath = ' } #[DataProvider('providerGetJSignParam')] - public function testGetJSignParam(string $temp_path, string $java_path, string $jar_path, bool $throwException): void { + public function testGetJSignParam(string $temp_path, string $java_path, string $jsignpdf_path, bool $throwException): void { $this->appConfig->setValueString('libresign', 'jsignpdf_home', '/'); $this->appConfig->setValueString('libresign', 'java_path', $java_path); $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', $temp_path); - $this->appConfig->setValueString('libresign', 'jsignpdf_jar_path', $jar_path); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', $jsignpdf_path); $this->javaHelper->method('getJavaPath')->willReturn($java_path); - $expected = new JSignParam(); - if ($java_path) { - $expected->setJavaPath("JSIGNPDF_HOME='/' $java_path -Duser.home='/' "); - } - $expected->setTempPath($temp_path); - $expected->setjSignPdfJarPath($jar_path); - $jSignPdfHandler = $this->getInstance(); if ($throwException) { $this->expectException(\Exception::class); - $jSignParam = $jSignPdfHandler->getJSignParam(); + $jSignPdfHandler->getJSignParam(); + return; + } + $jSignParam = $jSignPdfHandler->getJSignParam(); + $this->assertSame('', $jSignParam->getPdf()); + if ($java_path === '') { + $this->assertTrue($jSignParam->isUseJavaInstalled()); } else { - $jSignParam = $jSignPdfHandler->getJSignParam(); - $this->assertEquals($expected->getPdf(), $jSignParam->getPdf()); - $this->assertEquals($expected->getJavaPath(), $jSignParam->getJavaPath()); - $this->assertEquals($expected->getTempPath(), $jSignParam->getTempPath()); - $this->assertEquals($expected->getjSignPdfJarPath(), $jSignParam->getjSignPdfJarPath()); - $this->assertEquals('-a -kst PKCS12', $jSignParam->getJSignParameters()); + $this->assertFalse($jSignParam->isUseJavaInstalled()); + $this->assertSame($java_path, $jSignParam->getJavaPath()); } + $this->assertSame($temp_path, $jSignParam->getTempPath()); + $this->assertSame($jsignpdf_path, $jSignParam->getJSignPdfPath()); + $this->assertSame(['-Duser.home=/'], $jSignParam->getJavaOptions()); + $this->assertSame(['JSIGNPDF_HOME' => '/'], $jSignParam->getEnvironmentVariables()); + $this->assertSame("'-a' '-kst' 'PKCS12'", $jSignParam->getJSignParameters()); } public static function providerGetJSignParam(): array { return [ - ['', '', __FILE__, true], - ['invalid', '', __FILE__, true], - [sys_get_temp_dir(), '', __FILE__, false], - [sys_get_temp_dir(), 'b', __FILE__, true], - [sys_get_temp_dir(), __FILE__, __FILE__, false], - [sys_get_temp_dir(), 'b', __FILE__, true], - [sys_get_temp_dir(), __FILE__, __FILE__, false], - [sys_get_temp_dir(), __FILE__, '', true], + 'temp path empty' => ['', '', __DIR__, true], + 'temp path not writable' => ['invalid', '', __DIR__, true], + 'system java' => [sys_get_temp_dir(), '', __DIR__, false], + 'java binary missing' => [sys_get_temp_dir(), 'b', __DIR__, true], + 'downloaded java' => [sys_get_temp_dir(), __FILE__, __DIR__, false], + 'jsignpdf path not configured' => [sys_get_temp_dir(), __FILE__, '', true], + 'jsignpdf path is a file, not the extracted directory' => [sys_get_temp_dir(), __FILE__, __FILE__, true], ]; } @@ -881,7 +938,7 @@ public function testGetSignatureTextWithTwigDateFilterAndTimezone(): void { $jSignPdfHandler = $this->getInstance(); $actual = $jSignPdfHandler->getSignatureText(); - $this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} [A-Z]{3,4}"$/', $actual); + $this->assertMatchesRegularExpression('/^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2} [A-Z]{3,4}$/', $actual); } public function testGetSignatureTextWithTwigDateFilterWithoutTimezone(): void { @@ -893,7 +950,7 @@ public function testGetSignatureTextWithTwigDateFilterWithoutTimezone(): void { $jSignPdfHandler = $this->getInstance(); $actual = $jSignPdfHandler->getSignatureText(); - $this->assertMatchesRegularExpression('/^"\d{2}\/\d{2}\/\d{4}"$/', $actual); + $this->assertMatchesRegularExpression('/^\d{2}\/\d{2}\/\d{4}$/', $actual); } public function testGetSignatureTextGraphicOnlyWithTwigDateFilterAlwaysReturnsEmpty(): void { @@ -905,25 +962,27 @@ public function testGetSignatureTextGraphicOnlyWithTwigDateFilterAlwaysReturnsEm $jSignPdfHandler = $this->getInstance(); $actual = $jSignPdfHandler->getSignatureText(); - $this->assertSame('""', $actual); + $this->assertSame('', $actual); } public static function providerGetSignatureText(): array { return [ - [SignerElementsService::RENDER_MODE_DEFAULT, '', '""'], - [SignerElementsService::RENDER_MODE_DEFAULT, 'a', '"a"'], - [SignerElementsService::RENDER_MODE_DEFAULT, "a\na", "\"a\na\""], - [SignerElementsService::RENDER_MODE_DEFAULT, 'a"a', '"a\"a"'], - [SignerElementsService::RENDER_MODE_DEFAULT, 'a$a', '"a\$a"'], + // The text reaches the wrapper as is: the wrapper escapes it for the shell. + [SignerElementsService::RENDER_MODE_DEFAULT, '', ''], + [SignerElementsService::RENDER_MODE_DEFAULT, 'a', 'a'], + [SignerElementsService::RENDER_MODE_DEFAULT, "a\na", "a\na"], + [SignerElementsService::RENDER_MODE_DEFAULT, 'a"a', 'a"a'], + [SignerElementsService::RENDER_MODE_DEFAULT, "a'a", "a'a"], + [SignerElementsService::RENDER_MODE_DEFAULT, 'a$a', 'a$a'], // Plain {{ServerSignatureDate}} (no spaces) preserves JSign placeholder - [SignerElementsService::RENDER_MODE_DEFAULT, '{{ServerSignatureDate}}', '"\${timestamp}"'], + [SignerElementsService::RENDER_MODE_DEFAULT, '{{ServerSignatureDate}}', '${timestamp}'], // Plain {{ ServerSignatureDate }} (with spaces) also preserves JSign placeholder - [SignerElementsService::RENDER_MODE_DEFAULT, '{{ ServerSignatureDate }}', '"\${timestamp}"'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, '', '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a', '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, "a\na", '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a"a', '""'], - [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a$a', '""'], + [SignerElementsService::RENDER_MODE_DEFAULT, '{{ ServerSignatureDate }}', '${timestamp}'], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, '', ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a', ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, "a\na", ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a"a', ''], + [SignerElementsService::RENDER_MODE_GRAPHIC_ONLY, 'a$a', ''], ]; } @@ -948,4 +1007,209 @@ public function testCheckTsaErrorUnknownHostMentionsDnsNetworkFirewall(): void { 'TSAClientBouncyCastle: java.net.UnknownHostException: invalid-tsa.example.com', ]); } + + #[DataProvider('providerTsaParameters')] + public function testTsaParametersAndPassword(array $tsaSettings, string $storedPassword, array $expectedParameters, array $expectedPasswords): void { + if (self::$certificateEngineFactory === null || empty(self::$certificateContent)) { + $this->markTestSkipped('Certificate initialization failed'); + } + $this->resolvedPolicyValues[TsaPolicy::KEY] = TsaPolicyValue::encode($tsaSettings); + $this->appConfig->setValueString('libresign', TsaPolicy::PASSWORD_APP_CONFIG_KEY, $storedPassword); + $this->appConfig->setValueString('libresign', 'java_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); + $this->persistHashAlgorithmPolicy('SHA256'); + + $inputFile = $this->createMock(\OC\Files\Node\File::class); + $inputFile->method('getContent')->willReturn('%PDF-1.6'); + + $paramsSeen = []; + $mock = $this->createMock(JSignPDF::class); + $mock->method('setParam') + ->willReturnCallback(function (JSignParam $param) use (&$paramsSeen): void { + $paramsSeen[] = $param; + }); + $mock->method('sign')->willReturn('content'); + + $jSignPdfHandler = $this->getInstance(); + $jSignPdfHandler->setJSignPdf($mock); + $jSignPdfHandler->setInputFile($inputFile); + $jSignPdfHandler->setCertificate(self::$certificateContent); + $jSignPdfHandler->setPassword('password'); + $jSignPdfHandler->getSignedContent(); + + $this->assertCount(1, $paramsSeen); + $this->assertSame( + self::expectedJSignParameters($expectedParameters + ['--hash-algorithm' => 'SHA256']), + $paramsSeen[0]->getJSignParameters(), + ); + $this->assertSame($expectedPasswords, $paramsSeen[0]->getPasswords()); + if ($storedPassword !== '') { + $this->assertStringNotContainsString($storedPassword, $paramsSeen[0]->getJSignParameters()); + } + } + + public static function providerTsaParameters(): array { + $tsa = [ + 'url' => 'https://tsa.example.test/tsr', + 'policy_oid' => '1.2.3.4', + 'auth_type' => 'basic', + 'username' => 'alice', + ]; + return [ + 'no TSA configured' => [ + ['url' => ''], + 'tsa secret', + [], + [], + ], + 'url only' => [ + ['url' => 'https://tsa.example.test/tsr'], + '', + ['--tsa-server-url' => 'https://tsa.example.test/tsr'], + [], + ], + 'url with policy OID and no authentication' => [ + ['url' => 'https://tsa.example.test/tsr', 'policy_oid' => '1.2.3.4', 'auth_type' => 'none'], + 'tsa secret', + ['--tsa-server-url' => 'https://tsa.example.test/tsr', '--tsa-policy-oid' => '1.2.3.4'], + [], + ], + 'basic authentication: user on the command line, password over stdin' => [ + $tsa, + 'tsa secret', + [ + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--tsa-policy-oid' => '1.2.3.4', + '--tsa-authentication' => 'PASSWORD', + '--tsa-user' => 'alice', + ], + ['-tsp' => 'tsa secret'], + ], + 'basic authentication with shell characters in the password' => [ + $tsa, + "p4\$s 'w\"ord", + [ + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--tsa-policy-oid' => '1.2.3.4', + '--tsa-authentication' => 'PASSWORD', + '--tsa-user' => 'alice', + ], + ['-tsp' => "p4\$s 'w\"ord"], + ], + 'basic authentication without a stored password is skipped' => [ + $tsa, + '', + ['--tsa-server-url' => 'https://tsa.example.test/tsr', '--tsa-policy-oid' => '1.2.3.4'], + [], + ], + 'basic authentication without a URL sends nothing' => [ + ['url' => '', 'auth_type' => 'basic', 'username' => 'alice'], + 'tsa secret', + [], + [], + ], + 'basic authentication without a username is skipped' => [ + ['url' => 'https://tsa.example.test/tsr', 'auth_type' => 'basic', 'username' => ''], + 'tsa secret', + ['--tsa-server-url' => 'https://tsa.example.test/tsr'], + [], + ], + ]; + } + + #[DataProvider('providerCertificationLevelWithoutVisibleElements')] + public function testCertificationLevelWithoutVisibleElements(bool $docMdpEnabled, array $visibleElements, string $pdfContent, array $tsaSettings, array $expectedParameters): void { + if (self::$certificateEngineFactory === null || empty(self::$certificateContent)) { + $this->markTestSkipped('Certificate initialization failed'); + } + $this->resolvedPolicyValues[TsaPolicy::KEY] = TsaPolicyValue::encode($tsaSettings); + $this->appConfig->setValueString('libresign', 'java_path', __FILE__); + $this->appConfig->setValueString('libresign', 'jsignpdf_temp_path', sys_get_temp_dir()); + $this->appConfig->setValueString('libresign', 'jsignpdf_path', __DIR__); + $this->persistSignatureStampPolicy('', SignerElementsService::RENDER_MODE_DESCRIPTION_ONLY, 10, SignatureTextPolicyValue::DEFAULT_SIGNATURE_FONT_SIZE, 100, 100, 'deleted'); + $this->persistHashAlgorithmPolicy('SHA256'); + $this->signatureBackgroundService->method('getSignatureBackgroundType')->willReturn('deleted'); + + $inputFile = $this->createMock(\OC\Files\Node\File::class); + $inputFile->method('getContent')->willReturn($pdfContent); + + $paramsSeen = []; + $mock = $this->createMock(JSignPDF::class); + $mock->method('setParam') + ->willReturnCallback(function (JSignParam $param) use (&$paramsSeen): void { + $paramsSeen[] = $param->getJSignParameters(); + }); + $mock->method('sign')->willReturn('content'); + + $docMdpConfigService = $this->createMock(DocMdpConfigService::class); + $docMdpConfigService->method('isEnabled')->willReturn($docMdpEnabled); + $docMdpConfigService->method('getLevel')->willReturn(DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS); + + $jSignPdfHandler = $this->getInstance(); + $this->setDocMdpConfigService($jSignPdfHandler, $docMdpConfigService); + $jSignPdfHandler->setVisibleElements($visibleElements); + $jSignPdfHandler->setJSignPdf($mock); + $jSignPdfHandler->setInputFile($inputFile); + $jSignPdfHandler->setSignatureParams(['SignerCommonName' => 'Test User']); + $jSignPdfHandler->setCertificate(self::$certificateContent); + $jSignPdfHandler->setPassword('password'); + $jSignPdfHandler->getSignedContent(); + + $this->assertCount(1, $paramsSeen); + $paramsAsOptions = preg_replace('/\\/\S+app-dark.png/', 'signature.png', $paramsSeen[0]); + $this->assertSame(self::expectedJSignParameters($expectedParameters), $paramsAsOptions); + } + + public static function providerCertificationLevelWithoutVisibleElements(): array { + $element = self::getElement([ + 'page' => 1, + 'llx' => 10, + 'lly' => 10, + 'urx' => 110, + 'ury' => 60, + ], realpath(__DIR__ . '/../../../../../img/app-dark.png')); + $tsa = ['url' => 'https://tsa.example.test/tsr']; + return [ + 'certification before the TSA options when the PDF has no signature' => [ + true, + [], + '%PDF-1.6', + $tsa, + ['-cl' => DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, '--tsa-server-url' => 'https://tsa.example.test/tsr', '--hash-algorithm' => 'SHA256'], + ], + 'no certification when the PDF already has a signature' => [ + true, + [], + "%PDF-1.6\n/ByteRange [0 0 0 0]", + $tsa, + ['--tsa-server-url' => 'https://tsa.example.test/tsr', '--hash-algorithm' => 'SHA256'], + ], + 'no certification when DocMDP is disabled, even with a visible element on a signed PDF' => [ + false, + [$element], + "%PDF-1.6\n/ByteRange [0 0 0 0]", + ['url' => ''], + ['--hash-algorithm' => 'SHA256', '--l2-text' => '', '-V', '-llx' => '10', '-lly' => '10', '-urx' => '110', '-ury' => '60', '--bg-path' => 'signature.png'], + ], + ]; + } + + #[DataProvider('providerToJSignParameters')] + public function testToJSignParameters(array $params, array $expected): void { + $jSignPdfHandler = $this->getInstance(); + + $this->assertSame($expected, self::invokePrivate($jSignPdfHandler, 'toJSignParameters', [$params])); + } + + public static function providerToJSignParameters(): array { + return [ + 'null is a flag' => [['-V' => null], ['-V']], + 'integers become strings' => [['-pg' => 2, '-llx' => 0], ['-pg' => '2', '-llx' => '0']], + 'floats become strings' => [['--font-size' => 16.5, '--bg-scale' => 1.0], ['--font-size' => '16.5', '--bg-scale' => '1']], + 'empty string is a value' => [['--l2-text' => ''], ['--l2-text' => '']], + 'text is kept as is' => [['--l2-text' => 'a"b $c \'d'], ['--l2-text' => 'a"b $c \'d']], + 'order is preserved' => [['-a' => null, '-kst' => 'PKCS12', '-cl' => 'CERTIFIED_NO_CHANGES_ALLOWED'], ['-a', '-kst' => 'PKCS12', '-cl' => 'CERTIFIED_NO_CHANGES_ALLOWED']], + ]; + } } diff --git a/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php b/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php new file mode 100644 index 0000000000..a647f6fb14 --- /dev/null +++ b/tests/php/Unit/Service/Install/JSignPdfReleaseTest.php @@ -0,0 +1,61 @@ +assertSame('3.1.0', JSignPdfRelease::VERSION); + $this->assertSame('jsignpdf-' . JSignPdfRelease::VERSION . '-minimal.zip', JSignPdfRelease::archiveName()); + } + + public function testDownloadUrlPointsToTheArchiveOfTheReleaseTag(): void { + $this->assertSame( + self::releaseUrl() . 'jsignpdf-' . JSignPdfRelease::VERSION . '-minimal.zip', + JSignPdfRelease::downloadUrl(), + ); + } + + public function testChecksumUrlPointsToTheSha256SumsOfTheSameRelease(): void { + $this->assertSame( + self::releaseUrl() . 'jsignpdf-' . JSignPdfRelease::VERSION . '-SHA256SUMS.txt', + JSignPdfRelease::checksumUrl(), + ); + } + + #[DataProvider('providerInstallPath')] + public function testInstallPathIsTheVersionedDirectoryInsideTheExtractDir(string $extractDir, string $expected): void { + $this->assertSame($expected, JSignPdfRelease::installPath($extractDir)); + } + + public static function providerInstallPath(): array { + return [ + 'appdata folder' => [ + '/var/www/html/data/appdata_abc/libresign/x86_64/jsignpdf', + '/var/www/html/data/appdata_abc/libresign/x86_64/jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION, + ], + 'path with spaces' => [ + '/opt/libre sign/jsignpdf', + '/opt/libre sign/jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION, + ], + 'relative path' => [ + 'jsignpdf', + 'jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION, + ], + ]; + } +} diff --git a/tests/php/Unit/Service/Install/SignSetupServiceTest.php b/tests/php/Unit/Service/Install/SignSetupServiceTest.php index f13dd6b76f..7ff050b1df 100644 --- a/tests/php/Unit/Service/Install/SignSetupServiceTest.php +++ b/tests/php/Unit/Service/Install/SignSetupServiceTest.php @@ -12,6 +12,7 @@ use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper; use OCA\Libresign\AppInfo\Application; +use OCA\Libresign\Service\Install\JSignPdfRelease; use OCA\Libresign\Service\Install\SignSetupService; use OCP\App\IAppManager; use OCP\Files\AppData\IAppDataFactory; @@ -199,7 +200,7 @@ public function testVerify(): void { #[DataProvider('dataGetInstallPath')] public function testGetInstallPath(string $architecture, string $resource, string $distro, string $expected): void { $this->appConfig->setValueString(Application::APP_ID, 'java_path', 'vfs://home/data/appdata_1/libresign/x86_64/linux/java/jdk-21.0.2+13-jre/bin/java'); - $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_jar_path', 'vfs://home/data/appdata_1/libresign/x86_64/jsignpdf/jsignpdf-2.2.2/JSignPdf.jar'); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', 'vfs://home/data/appdata_1/libresign/x86_64/jsignpdf/jsignpdf-' . JSignPdfRelease::VERSION); $this->appConfig->setValueString(Application::APP_ID, 'pdftk_path', 'vfs://home/data/appdata_1/libresign/x86_64/pdftk/pdftk.jar'); $this->appConfig->setValueString(Application::APP_ID, 'cfssl_bin', 'vfs://home/data/appdata_1/libresign/x86_64/cfssl/cfssl'); $actual = $this->getInstance() diff --git a/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php b/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php index 3aa551278a..9e56a4fdf3 100644 --- a/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php +++ b/tests/php/Unit/SetupCheck/JSignPdfSetupCheckTest.php @@ -14,11 +14,18 @@ function file_exists(string $filename): bool { } } +if (!function_exists('OCA\Libresign\SetupCheck\is_dir')) { + function is_dir(string $filename): bool { + return \OCA\Libresign\Tests\Mock\FileSystemMock::fileExists($filename); + } +} + namespace OCA\Libresign\Tests\Unit\SetupCheck; use OCA\Libresign\Handler\SignEngine\JSignPdfHandler; use OCA\Libresign\Helper\JavaHelper; use OCA\Libresign\Service\Install\InstallService; +use OCA\Libresign\Service\Install\JSignPdfRelease; use OCA\Libresign\Service\Install\SignSetupService; use OCA\Libresign\SetupCheck\JSignPdfSetupCheck; use OCA\Libresign\Tests\Mock\FileSystemMock; @@ -95,7 +102,7 @@ public function testGetCategory(): void { public function testRunNoPathConfigured(): void { $this->mockTranslation(); $this->appConfig->method('getValueString') - ->with('libresign', 'jsignpdf_jar_path') + ->with('libresign', 'jsignpdf_path') ->willReturn(''); $result = $this->check->run(); @@ -134,9 +141,9 @@ public function testRunVerifyFails(): void { public function testRunBinaryNotFound(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); @@ -144,26 +151,26 @@ public function testRunBinaryNotFound(): void { $this->signSetupService->method('verify') ->willReturn([]); - FileSystemMock::$files[$jarPath] = false; + FileSystemMock::$files[$jsignPdfPath] = false; $result = $this->check->run(); $this->assertInstanceOf(SetupResult::class, $result); $this->assertSame('error', $result->getSeverity()); - $this->assertStringContainsString('JSignPdf file not found', $result->getDescription()); + $this->assertStringContainsString('JSignPdf path not found', $result->getDescription()); } public function testRunJavaNotFound(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify') ->willReturn([]); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; $this->javaHelper->method('getJavaPath') ->willReturn(''); @@ -175,6 +182,29 @@ public function testRunJavaNotFound(): void { $this->assertStringContainsString('Necessary Java to run JSignPdf', $result->getDescription()); } + public function testRunJavaBinaryMissing(): void { + $this->mockTranslation(); + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; + $this->appConfig->method('getValueString') + ->willReturn($jsignPdfPath); + $this->systemConfig->method('getSystemValueBool') + ->willReturn(false); + $this->signSetupService->method('verify') + ->willReturn([]); + + FileSystemMock::$files[$jsignPdfPath] = true; + FileSystemMock::$files['/opt/java/bin/java'] = false; + + $this->javaHelper->method('getJavaPath') + ->willReturn('/opt/java/bin/java'); + + $result = $this->check->run(); + + $this->assertInstanceOf(SetupResult::class, $result); + $this->assertSame('error', $result->getSeverity()); + $this->assertStringContainsString('Necessary Java to run JSignPdf', $result->getDescription()); + } + private function createJSignParamMock() { return $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\Sign\JSignParam::class) ->disableOriginalConstructor() @@ -183,14 +213,14 @@ private function createJSignParamMock() { public function testRunVersionEmpty(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -200,6 +230,7 @@ public function testRunVersionEmpty(): void { $jsignPdfMock->method('getVersion')->willReturn(''); $jsignParamMock = $this->createJSignParamMock(); + $jsignPdfMock->expects($this->once())->method('setParam')->with($jsignParamMock); $this->jSignPdfHandler->method('getJSignPdf')->willReturn($jsignPdfMock); $this->jSignPdfHandler->method('getJSignParam')->willReturn($jsignParamMock); @@ -213,14 +244,14 @@ public function testRunVersionEmpty(): void { public function testRunVersionTooLow(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -243,14 +274,14 @@ public function testRunVersionTooLow(): void { public function testRunVersionTooHigh(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -273,14 +304,14 @@ public function testRunVersionTooHigh(): void { public function testRunSuccess(): void { $this->mockTranslation(); - $jarPath = '/fake/path/jsignpdf.jar'; + $jsignPdfPath = '/fake/path/jsignpdf-' . JSignPdfRelease::VERSION; $this->appConfig->method('getValueString') - ->willReturn($jarPath); + ->willReturn($jsignPdfPath); $this->systemConfig->method('getSystemValueBool') ->willReturn(false); $this->signSetupService->method('verify')->willReturn([]); $this->javaHelper->method('getJavaPath')->willReturn('/usr/bin/java'); - FileSystemMock::$files[$jarPath] = true; + FileSystemMock::$files[$jsignPdfPath] = true; FileSystemMock::$files['/usr/bin/java'] = true; $jsignPdfMock = $this->getMockBuilder(\OCA\Libresign\Vendor\Jeidison\JSignPDF\JSignPDF::class) @@ -299,6 +330,6 @@ public function testRunSuccess(): void { $this->assertInstanceOf(SetupResult::class, $result); $this->assertSame('success', $result->getSeverity()); $this->assertStringContainsString('JSignPdf version: ' . InstallService::JSIGNPDF_VERSION, $result->getDescription()); - $this->assertStringContainsString('JSignPdf path: ' . $jarPath, $result->getDescription()); + $this->assertStringContainsString('JSignPdf path: ' . $jsignPdfPath, $result->getDescription()); } }