diff --git a/3rdparty b/3rdparty index 0a9bcd98a9..8652bc9fb5 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit 0a9bcd98a992cefdabf6a4760af527d565032e4c +Subproject commit 8652bc9fb56920f44467876a65dd40d79bb2994c diff --git a/lib/Handler/SignEngine/JSignPdfHandler.php b/lib/Handler/SignEngine/JSignPdfHandler.php index 361925055c..10e76a0c8e 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\SignatureBackgroundService; use OCA\Libresign\Service\SignatureTextService; use OCA\Libresign\Service\SignerElementsService; @@ -77,34 +76,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: @@ -256,36 +250,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); @@ -304,7 +290,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'], @@ -313,7 +299,7 @@ private function signUsingVisibleElements(string $normalizedPdf, string $hashAlg } $fontSize = $this->parseSignatureText()['templateFontSize']; - if ($fontSize === self::SIGNATURE_DEFAULT_FONT_SIZE || !$fontSize || $params['--l2-text'] === '""') { + if ($fontSize === self::SIGNATURE_DEFAULT_FONT_SIZE || !$fontSize || $params['--l2-text'] === '') { $fontSize = 0; } @@ -326,11 +312,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++; @@ -365,7 +349,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, @@ -400,16 +384,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); @@ -616,30 +599,33 @@ 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 { $tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', ''); if (empty($tsaUrl)) { @@ -663,13 +649,33 @@ private function getTsaParameters(): array { if (!empty($tsaUsername) && !empty($tsaPassword)) { $params['--tsa-authentication'] = 'PASSWORD'; $params['--tsa-user'] = $tsaUsername; - $params['--tsa-password'] = $tsaPassword; } } return $params; } + private function getTsaPassword(): string { + $tsaUrl = $this->appConfig->getValueString(Application::APP_ID, 'tsa_url', ''); + if ($tsaUrl === '') { + return ''; + } + + $tsaAuthType = $this->appConfig->getValueString(Application::APP_ID, 'tsa_auth_type', 'none'); + if ($tsaAuthType !== 'basic') { + return ''; + } + + $tsaUsername = $this->appConfig->getValueString(Application::APP_ID, 'tsa_username', ''); + $tsaPassword = $this->appConfig->getValueString(Application::APP_ID, 'tsa_password', ''); + + if ($tsaUsername === '' || $tsaPassword === '') { + return ''; + } + + return $tsaPassword; + } + private function signWrapper(JSignPDF $jSignPDF): string { try { return $jSignPDF->sign(); diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index 2bf04671b6..43d9be6a2e 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -477,36 +477,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 +534,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 +545,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 { @@ -203,7 +203,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 60f85d971e..baeef16287 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( $this->l10n->t('JSignPdf not found'), // TRANSLATORS Command to run into terminal using Nextcloud occ to configure LibreSign using CLI when the sysadmin want to do this by CLI. @@ -87,12 +87,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']) ); @@ -148,8 +148,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 a56b6f24da..5d45fbd4ac 100644 --- a/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/JSignPdfHandlerTest.php @@ -51,6 +51,7 @@ public static function setUpBeforeClass(): void { self::$certificateEngineFactory = \OCP\Server::get(CertificateEngineFactory::class); $appConfig = self::getMockAppConfig(); $appConfig->setValueString(Application::APP_ID, 'certificate_engine', 'openssl'); + \OCP\Server::get(CaIdentifierService::class)->generateCaId('openssl'); $certificateEngine = self::$certificateEngineFactory->getEngine(); $certificateEngine ->setConfigPath(\OCP\Server::get(ITempManager::class)->getTemporaryFolder('certificate')) @@ -251,7 +252,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'); @@ -260,7 +261,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( @@ -290,15 +296,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 { @@ -313,7 +332,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([ @@ -331,7 +350,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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([ @@ -349,7 +368,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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([ @@ -367,7 +386,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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 != 10' => [ 'visibleElements' => [self::getElement([ @@ -385,7 +404,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([ @@ -403,7 +422,43 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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' => 10, + '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([ @@ -421,7 +476,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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([ @@ -439,7 +494,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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([ @@ -457,7 +512,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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([ @@ -475,7 +530,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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(''). @@ -496,7 +551,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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([ @@ -514,7 +569,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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([ @@ -532,7 +587,7 @@ public static function providerSignAffectedParams(): array { 'templateFontSize' => 10, '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'] ], ]; } @@ -600,8 +655,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 { @@ -660,7 +715,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')] @@ -760,44 +815,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], ]; } @@ -821,7 +875,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 { @@ -835,7 +889,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 { @@ -849,25 +903,27 @@ public function testGetSignatureTextGraphicOnlyWithTwigDateFilterAlwaysReturnsEm $jSignPdfHandler = $this->getInstance(); $actual = $jSignPdfHandler->getSignatureText(); - $this->assertSame('""', $actual); + $this->assertSame('', $actual); } public static function providerGetSignatureText(): array { return [ - ['FAKE_RENDER_MODE', '', '""'], - ['FAKE_RENDER_MODE', 'a', '"a"'], - ['FAKE_RENDER_MODE', "a\na", "\"a\na\""], - ['FAKE_RENDER_MODE', 'a"a', '"a\"a"'], - ['FAKE_RENDER_MODE', 'a$a', '"a\$a"'], + // The text reaches the wrapper as is; the wrapper escapes it for the shell. + ['FAKE_RENDER_MODE', '', ''], + ['FAKE_RENDER_MODE', 'a', 'a'], + ['FAKE_RENDER_MODE', "a\na", "a\na"], + ['FAKE_RENDER_MODE', 'a"a', 'a"a'], + ['FAKE_RENDER_MODE', "a'a", "a'a"], + ['FAKE_RENDER_MODE', 'a$a', 'a$a'], // Plain {{ServerSignatureDate}} (no spaces) preserves JSign placeholder - ['FAKE_RENDER_MODE', '{{ServerSignatureDate}}', '"\${timestamp}"'], + ['FAKE_RENDER_MODE', '{{ServerSignatureDate}}', '${timestamp}'], // Plain {{ ServerSignatureDate }} (with spaces) also preserves JSign placeholder - ['FAKE_RENDER_MODE', '{{ ServerSignatureDate }}', '"\${timestamp}"'], - ['GRAPHIC_ONLY', '', '""'], - ['GRAPHIC_ONLY', 'a', '""'], - ['GRAPHIC_ONLY', "a\na", '""'], - ['GRAPHIC_ONLY', 'a"a', '""'], - ['GRAPHIC_ONLY', 'a$a', '""'], + ['FAKE_RENDER_MODE', '{{ ServerSignatureDate }}', '${timestamp}'], + ['GRAPHIC_ONLY', '', ''], + ['GRAPHIC_ONLY', 'a', ''], + ['GRAPHIC_ONLY', "a\na", ''], + ['GRAPHIC_ONLY', 'a"a', ''], + ['GRAPHIC_ONLY', 'a$a', ''], ]; } @@ -892,4 +948,240 @@ 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'); + } + + foreach ($tsaSettings as $key => $value) { + $this->appConfig->setValueString(Application::APP_ID, $key, $value); + } + + $this->appConfig->setValueString(Application::APP_ID, 'tsa_password', $storedPassword); + $this->appConfig->setValueString(Application::APP_ID, 'signature_hash_algorithm', 'SHA256'); + $this->appConfig->setValueString(Application::APP_ID, 'java_path', __FILE__); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_temp_path', sys_get_temp_dir()); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', __DIR__); + + $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 { + $basic = [ + 'tsa_url' => 'https://tsa.example.test/tsr', + 'tsa_policy_oid' => '1.2.3.4', + 'tsa_auth_type' => 'basic', + 'tsa_username' => 'alice', + ]; + + return [ + 'no TSA configured' => [ + ['tsa_url' => ''], + 'tsa secret', + [], + [], + ], + 'URL only' => [ + ['tsa_url' => 'https://tsa.example.test/tsr'], + '', + ['--tsa-server-url' => 'https://tsa.example.test/tsr'], + [], + ], + 'policy OID without authentication' => [ + [ + 'tsa_url' => 'https://tsa.example.test/tsr', + 'tsa_policy_oid' => '1.2.3.4', + 'tsa_auth_type' => 'none', + ], + '', + [ + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--tsa-policy-oid' => '1.2.3.4', + ], + [], + ], + 'basic authentication sends password over stdin' => [ + $basic, + '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 handles shell characters' => [ + $basic, + "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 password is skipped' => [ + $basic, + '', + [ + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--tsa-policy-oid' => '1.2.3.4', + ], + [], + ], + 'basic authentication without username is skipped' => [ + [ + 'tsa_url' => 'https://tsa.example.test/tsr', + 'tsa_auth_type' => 'basic', + 'tsa_username' => '', + ], + 'tsa secret', + ['--tsa-server-url' => 'https://tsa.example.test/tsr'], + [], + ], + ]; + } + + #[DataProvider('providerCertificationLevelWithoutVisibleElements')] + public function testCertificationLevelWithoutVisibleElements( + bool $docMdpEnabled, + string $pdfContent, + array $tsaSettings, + array $expectedParameters, + ): void { + if (self::$certificateEngineFactory === null || empty(self::$certificateContent)) { + $this->markTestSkipped('Certificate initialization failed'); + } + + foreach ($tsaSettings as $key => $value) { + $this->appConfig->setValueString(Application::APP_ID, $key, $value); + } + + $this->appConfig->setValueString(Application::APP_ID, 'signature_hash_algorithm', 'SHA256'); + $this->appConfig->setValueString(Application::APP_ID, 'java_path', __FILE__); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_temp_path', sys_get_temp_dir()); + $this->appConfig->setValueString(Application::APP_ID, 'jsignpdf_path', __DIR__); + + $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([]); + $jSignPdfHandler->setJSignPdf($mock); + $jSignPdfHandler->setInputFile($inputFile); + $jSignPdfHandler->setCertificate(self::$certificateContent); + $jSignPdfHandler->setPassword('password'); + $jSignPdfHandler->getSignedContent(); + + $this->assertCount(1, $paramsSeen); + $this->assertSame( + self::expectedJSignParameters($expectedParameters), + $paramsSeen[0], + ); + } + + public static function providerCertificationLevelWithoutVisibleElements(): array { + return [ + 'certification before TSA options' => [ + true, + '%PDF-1.6', + ['tsa_url' => 'https://tsa.example.test/tsr'], + [ + '-cl' => DocMdpLevel::CERTIFIED_FORM_FILLING_AND_ANNOTATIONS->name, + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--hash-algorithm' => 'SHA256', + ], + ], + 'no certification when PDF already has signature' => [ + true, + "%PDF-1.6\n/ByteRange [0 0 0 0]", + ['tsa_url' => 'https://tsa.example.test/tsr'], + [ + '--tsa-server-url' => 'https://tsa.example.test/tsr', + '--hash-algorithm' => 'SHA256', + ], + ], + 'no certification when DocMDP is disabled' => [ + false, + '%PDF-1.6', + ['tsa_url' => ''], + [ + '--hash-algorithm' => 'SHA256', + ], + ], + ]; + } + + #[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 645e560664..6cf69a95da 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; @@ -198,7 +199,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()); } }