diff --git a/.gitignore b/.gitignore index 1b3a0fd..d96eb0d 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +.vscode .phpunit.result.cache vendor /vendor-bin/**/vendor/ diff --git a/README.md b/README.md index 6c23ef4..68dc638 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ With JSignPDF bin: ```php $param->setjSignPdfJarPath('/path/to/jsignpdf'); ``` +With specific Java or JSignPdf version: +```php +$params->getJSignPdfDownloadUrl('the url to download the zip here'); +$params->setJavaDownloadUrl('the url to download the .tar.gz here'); +``` Without JSignPDF bin: ```bash diff --git a/example/index.php b/example/index.php index 3cbc4a8..dca3118 100644 --- a/example/index.php +++ b/example/index.php @@ -3,16 +3,34 @@ /** * @author Jeidison Farias */ -require_once "../src/JSignPDF.php"; +require_once __DIR__ . '/../vendor/autoload.php'; use Jeidison\JSignPDF\JSignPDF; use Jeidison\JSignPDF\Sign\JSignParam; +$password = '123'; + +$privateKey = openssl_pkey_new([ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, +]); + +$csr = openssl_csr_new(['commonName' => 'John Doe'], $privateKey, ['digest_alg' => 'sha256']); +$x509 = openssl_csr_sign($csr, null, $privateKey, 365); + +openssl_pkcs12_export( + $x509, + $pfxCertificateContent, + $privateKey, + $password, +); + $param = JSignParam::instance(); -$param->setCertificate(file_get_contents('../tests/resources/certificado.pfx')); -$param->setPdf(file_get_contents('../tests/resources/pdf-test.pdf')); -$param->setPassword('123'); + +$param->setCertificate($pfxCertificateContent); +$param->setPdf(file_get_contents(__DIR__ . '/../tests/resources/pdf-test.pdf')); +$param->setPassword($password); $jSignPdf = new JSignPDF($param); $fileSigned = $jSignPdf->sign(); -file_put_contents('../tmp/file_signed.pdf', $fileSigned); \ No newline at end of file +file_put_contents(__DIR__ . '/../tmp/file_signed.pdf', $fileSigned); diff --git a/src/JSignPDF.php b/src/JSignPDF.php index 508703e..11b12ae 100644 --- a/src/JSignPDF.php +++ b/src/JSignPDF.php @@ -13,13 +13,13 @@ class JSignPDF private $service; private $param; - public function __construct(JSignParam $param = null) + public function __construct(?JSignParam $param = null) { $this->service = new JSignService(); $this->param = $param; } - public static function instance(JSignParam $param = null) + public static function instance(?JSignParam $param = null) { return new self($param); } diff --git a/src/Runtime/JSignPdfRuntimeService.php b/src/Runtime/JSignPdfRuntimeService.php new file mode 100644 index 0000000..c746e53 --- /dev/null +++ b/src/Runtime/JSignPdfRuntimeService.php @@ -0,0 +1,105 @@ +getjSignPdfJarPath(); + $downloadUrl = $params->getJSignPdfDownloadUrl(); + + if ($jsignPdfPath && !$downloadUrl) { + if (file_exists($jsignPdfPath)) { + return $jsignPdfPath; + } + throw new InvalidArgumentException('Jar of JSignPDF not found on path: '. $jsignPdfPath); + } + + if ($downloadUrl && $jsignPdfPath) { + $baseDir = preg_replace('/\/JSignPdf.jar$/', '', $jsignPdfPath); + if (!is_dir($baseDir)) { + $ok = mkdir($baseDir, 0755, true); + if ($ok === false) { + throw new InvalidArgumentException('The JSignPdf base dir cannot be created: '. $baseDir); + } + } + if (!file_exists($jsignPdfPath) || !self::validateVersion($params)) { + self::downloadAndExtract($params); + } + return $jsignPdfPath; + } + + throw new InvalidArgumentException('Java not found.'); + } + + private function validateVersion(JSignParam $params): bool + { + $jsignPdfPath = $params->getjSignPdfJarPath(); + $versionCacheFile = $jsignPdfPath . '/.jsignpdf_version_' . basename($params->getJSignPdfDownloadUrl()); + return file_exists($versionCacheFile); + } + + private function downloadAndExtract(JSignParam $params): void + { + $jsignPdfPath = $params->getjSignPdfJarPath(); + $url = $params->getJSignPdfDownloadUrl(); + + $baseDir = preg_replace('/\/JSignPdf.jar$/', '', $jsignPdfPath); + + if (!is_dir($baseDir)) { + $ok = mkdir($baseDir, 0755, true); + if (!$ok) { + throw new RuntimeException('Failure to create the folder: ' . $baseDir); + } + } + if (!filter_var($url, FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException('The url to download Java is invalid: ' . $url); + } + $this->chunkDownload($url, $baseDir . '/jsignpdf.zip'); + $z = new ZipArchive(); + $ok = $z->open($baseDir . '/jsignpdf.zip'); + if ($ok !== true) { + throw new InvalidArgumentException('The file ' . $baseDir . '/jsignpdf.zip cannot be extracted'); + } + $ok = $z->extractTo(pathto: $baseDir, files: [$z->getNameIndex(0) . 'JSignPdf.jar']); + if ($ok !== true) { + throw new InvalidArgumentException('JSignPdf.jar not found inside path: ' . $z->getNameIndex(0) . 'JSignPdf.jar'); + } + @exec('mv ' . escapeshellarg($baseDir . '/'. $z->getNameIndex(0)) . '/JSignPdf.jar ' . escapeshellarg($baseDir)); + @exec('rm -rf ' . escapeshellarg($baseDir . '/'. $z->getNameIndex(0))); + @exec('rm -f ' . escapeshellarg($baseDir) . '/.jsignpdf_version_*'); + unlink($baseDir . '/jsignpdf.zip'); + if (!file_exists($baseDir . '/JSignPdf.jar')) { + throw new RuntimeException('Java binary not found at: ' . $baseDir . '/bin/java'); + } + touch($baseDir . '/.jsignpdf_version_' . basename($url)); + } + + private function chunkDownload(string $url, string $destination): void + { + $fp = fopen($destination, 'w'); + + if ($fp) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + $response = curl_exec($ch); + if ($response === false) { + throw new InvalidArgumentException('Failure to download file using the url ' . $url); + } + curl_close($ch); + fclose($fp); + } else { + throw new InvalidArgumentException("Failute to download file using the url $url"); + } + } +} diff --git a/src/Runtime/JavaRuntimeService.php b/src/Runtime/JavaRuntimeService.php new file mode 100644 index 0000000..15c6283 --- /dev/null +++ b/src/Runtime/JavaRuntimeService.php @@ -0,0 +1,128 @@ +isUseJavaInstalled()) { + return 'java'; + } + + $javaPath = $params->getJavaPath(); + $downloadUrl = $params->getJavaDownloadUrl(); + + if ($javaPath && !$downloadUrl) { + if (is_file($javaPath) && is_executable($javaPath)) { + return $javaPath; + } + throw new InvalidArgumentException('Java path defined is not executable: ' . $javaPath); + } + + if ($downloadUrl && $javaPath) { + $baseDir = preg_replace('/\/bin\/java$/', '', $javaPath); + if (!is_dir($baseDir)) { + $ok = mkdir($baseDir, 0755, true); + if ($ok === false) { + throw new InvalidArgumentException('The java base dir is not a real directory. Create this directory first: '. $baseDir); + } + } + if (!self::validateVersion($params)) { + self::downloadAndExtract($downloadUrl, $javaPath); + } + $params->setJavaDownloadUrl(''); + $params->setJavaPath($javaPath); + return $javaPath; + } + + throw new InvalidArgumentException('Java not found.'); + } + + private function validateVersion(JSignParam $params): bool + { + $javaPath = $params->getJavaPath(); + $baseDir = preg_replace('/\/bin\/java$/', '', $javaPath); + $lastVersion = $baseDir . '/.java_version_' . basename($params->getJavaDownloadUrl()); + return file_exists($lastVersion); + } + + private function downloadAndExtract(string $url, string $baseDir): void + { + $baseDir = preg_replace('/\/bin\/java$/', '', $baseDir); + + if (!is_dir($baseDir)) { + $ok = mkdir($baseDir, 0755, true); + if (!$ok) { + throw new RuntimeException('Failure to create the folder: ' . $baseDir); + } + } + if (!filter_var($url, FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException('The url to download Java is invalid: ' . $url); + } + $this->chunkDownload($url, $baseDir . '/java.tar.gz'); + try { + $tar = new PharData($baseDir . '/java.tar.gz'); + } catch (PharException|UnexpectedValueException $e) { + throw new InvalidArgumentException('The file ' . $baseDir . '/java.tar.gz cannot be extracted'); + } + $rootDirInsideTar = $this->findRootDir($tar, $baseDir . '/java.tar.gz'); + if (!$rootDirInsideTar) { + throw new InvalidArgumentException('Invalid tar content.'); + } + $tar->extractTo(directory: $baseDir, overwrite: true); + @exec('mv ' . escapeshellarg($baseDir . '/'. $rootDirInsideTar) . '/* ' . escapeshellarg($baseDir)); + @exec('rm -rf ' . escapeshellarg($baseDir . '/'. $rootDirInsideTar)); + @exec('rm -f ' . escapeshellarg($baseDir) . '/.java_version_*'); + unlink($baseDir . '/java.tar.gz'); + touch($baseDir . '/.java_version_' . basename($url)); + if (!file_exists($baseDir . '/bin/java')) { + throw new RuntimeException('Java binary not found at: ' . $baseDir . '/bin/java'); + } + chmod($baseDir . '/bin/java', 0700); + } + + private function findRootDir(PharData $phar, $rootDir) { + $files = new \RecursiveIteratorIterator($phar, \RecursiveIteratorIterator::CHILD_FIRST); + $rootDir = realpath($rootDir); + + foreach ($files as $file) { + $pathName = $file->getPathname(); + if (str_contains($pathName, '/bin/') || str_contains($pathName, '/bin/')) { + $parts = explode($rootDir, $pathName); + $internalFullPath = end($parts); + $parts = explode('/bin/', $internalFullPath); + return trim($parts[0], '/'); + } + } + } + + private function chunkDownload(string $url, string $destination): void + { + $fp = fopen($destination, 'w'); + + if ($fp) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + $response = curl_exec($ch); + if ($response === false) { + throw new InvalidArgumentException('Failure to download file using the url ' . $url); + } + curl_close($ch); + fclose($fp); + } else { + throw new InvalidArgumentException("Failute to download file using the url $url"); + } + } +} diff --git a/src/Sign/JSignParam.php b/src/Sign/JSignParam.php index 576791f..692dc6d 100644 --- a/src/Sign/JSignParam.php +++ b/src/Sign/JSignParam.php @@ -13,16 +13,20 @@ class JSignParam private $pathPdfSigned; private $JSignParameters = "-a -kst PKCS12"; private $isUseJavaInstalled = false; - private $javaPath = ''; + private string $javaPath = ''; private $tempPath; private $tempName; private $isOutputTypeBase64 = false; - private $jSignPdfJarPath; + private string $jSignPdfJarPath; + private string $javaDownloadUrl = 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.8%2B9/OpenJDK21U-jre_x64_linux_hotspot_21.0.8_9.tar.gz'; + private string $jSignPdfDownloadUrl = 'https://github.com/intoolswetrust/jsignpdf/releases/download/JSignPdf_2_3_0/jsignpdf-2.3.0.zip'; public function __construct() { $this->tempName = md5(time() . uniqid() . mt_rand()); $this->tempPath = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR; + $this->javaPath = $this->tempPath . 'java' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'java'; + $this->jSignPdfJarPath = $this->tempPath . 'jsignpdf' . DIRECTORY_SEPARATOR . 'JSignPdf.jar'; } public static function instance() @@ -158,4 +162,25 @@ public function getTempCertificatePath() return $this->getTempPath() . $this->getTempName('.pfx'); } + public function setJavaDownloadUrl(string $url): self + { + $this->javaDownloadUrl = $url; + return $this; + } + + public function getJavaDownloadUrl(): string + { + return $this->javaDownloadUrl; + } + + public function setJSignPdfDownloadUrl(string $url): self + { + $this->jSignPdfDownloadUrl = $url; + return $this; + } + + public function getJSignPdfDownloadUrl(): string + { + return $this->jSignPdfDownloadUrl; + } } diff --git a/src/Sign/JSignService.php b/src/Sign/JSignService.php index fae57ce..27eaeae 100644 --- a/src/Sign/JSignService.php +++ b/src/Sign/JSignService.php @@ -4,6 +4,8 @@ use Exception; use Jeidison\JSignPDF\JSignFileService; +use Jeidison\JSignPDF\Runtime\JavaRuntimeService; +use Jeidison\JSignPDF\Runtime\JSignPdfRuntimeService; use Throwable; /** @@ -69,14 +71,11 @@ private function repackCertificateIfPasswordIsUnicode(JSignParam $params, $cert, public function getVersion(JSignParam $params) { $java = $this->javaCommand($params); + $jSignPdf = $this->getjSignPdfJarPath($params); $jSignPdf = $params->getjSignPdfJarPath(); - if (!$jSignPdf && class_exists('JSignPDF\JSignPDFBin\JSignPdfPathService')) { - $jSignPdf = \JSignPDF\JSignPDFBin\JSignPdfPathService::jSignPdfJarPath(); - } - $this->throwIf(!file_exists($jSignPdf), 'Jar of JSignPDF not found on path: '. $jSignPdf); $command = "$java -jar $jSignPdf --version 2>&1"; - \exec($command, $output); + exec($command, $output); $lastRow = end($output); if (empty($output) || strpos($lastRow, 'version') === false) { return ''; @@ -120,28 +119,22 @@ private function commandSign(JSignParam $params) { list ($pdf, $certificate) = $this->storeTempFiles($params); $java = $this->javaCommand($params); - $jSignPdf = $params->getjSignPdfJarPath(); - if (!$jSignPdf && class_exists('JSignPDF\JSignPDFBin\JSignPdfPathService')) { - $jSignPdf = \JSignPDF\JSignPDFBin\JSignPdfPathService::jSignPdfJarPath(); - } - $this->throwIf(!file_exists($jSignPdf), 'Jar of JSignPDF not found on path: '. $jSignPdf); + $jSignPdf = $this->getjSignPdfJarPath($params); $password = escapeshellarg($params->getPassword()); return "$java -Duser.language=en -jar $jSignPdf $pdf -ksf $certificate -ksp {$password} {$params->getJSignParameters()} -d {$params->getPathPdfSigned()} 2>&1"; } - private function javaCommand(JSignParam $params) + private function javaCommand(JSignParam $params): string { - if ($params->isUseJavaInstalled()) { - return 'java'; - } - if ($params->getJavaPath()) { - return $params->getJavaPath(); - } - if (!class_exists('JSignPDF\JSignPDFBin\JavaCommandService')) { - throw new Exception("JSignPDF not found, install manually or run composer require jsignpdf/jsignpdf-bin", 1); - } - return \JSignPDF\JSignPDFBin\JavaCommandService::instance()->command($params->isUseJavaInstalled()); + $javaRuntimeService = new JavaRuntimeService(); + return $javaRuntimeService->getPath($params); + } + + private function getjSignPdfJarPath(JSignParam $params): string + { + $JsignPdfRuntimeService = new JSignPdfRuntimeService(); + return $JsignPdfRuntimeService->getPath($params); } private function throwIf($condition, $message) diff --git a/tests/JSignPDFTest.php b/tests/JSignPDFTest.php index d0298dc..7ed04fc 100644 --- a/tests/JSignPDFTest.php +++ b/tests/JSignPDFTest.php @@ -13,6 +13,7 @@ function exec(string $command, ?array &$output = null, ?int &$return_var = null) namespace Jeidison\JSignPDF\Tests; +use org\bovigo\vfs\vfsStream; use Exception; use Jeidison\JSignPDF\Sign\JSignService; use Jeidison\JSignPDF\Tests\Builder\JSignParamBuilder; @@ -24,7 +25,7 @@ function exec(string $command, ?array &$output = null, ?int &$return_var = null) */ class JSignPDFTest extends TestCase { - private $service; + private JSignService $service; protected function setUp(): void { @@ -33,7 +34,7 @@ protected function setUp(): void $this->service = new JSignService(); } - private function getNewCert($password) + private function getNewCert($password, $expireDays = 365) { $privateKey = openssl_pkey_new([ 'private_key_bits' => 2048, @@ -43,54 +44,61 @@ private function getNewCert($password) $csrNames = ['commonName' => 'Jhon Doe']; $csr = openssl_csr_new($csrNames, $privateKey, ['digest_alg' => 'sha256']); - $x509 = openssl_csr_sign($csr, null, $privateKey, $days = 365, ['digest_alg' => 'sha256']); + $x509 = openssl_csr_sign($csr, null, $privateKey, $expireDays); - openssl_x509_export($x509, $rootCertificate); - openssl_pkey_export($privateKey, $rootPrivateKey); - - $privateKey = openssl_pkey_new([ - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]); - $temporaryFile = tempnam(sys_get_temp_dir(), 'cfg'); - $csr = openssl_csr_new($csrNames, $privateKey); - $x509 = openssl_csr_sign($csr, $rootCertificate, $rootPrivateKey, 365); - $certContent = null; openssl_pkcs12_export( $x509, - $certContent, + $pfxCertificateContent, $privateKey, $password, ); - return $certContent; + return $pfxCertificateContent; } public function testSignSuccess() { - if (!class_exists('JSignPDF\JSignPDFBin\JavaCommandService')) { - $this->markTestSkipped('Install jsignpdf/jsignpdf-bin'); - } + global $mockExec; + $mockExec = ['Finished: Signature succesfully created.']; $params = JSignParamBuilder::instance()->withDefault(); + vfsStream::setup('download'); + mkdir('vfs://download/jvava/bin', 0755, true); + touch('vfs://download/jvava/bin/java'); + chmod('vfs://download/jvava/bin/java', 0755); + $params->setJavaPath('vfs://download/jvava/bin/java'); + $params->setJavaDownloadUrl(''); + mkdir('vfs://download/jsignpdf', 0755, true); + $params->setjSignPdfJarPath('vfs://download/jsignpdf'); + $params->setJSignPdfDownloadUrl(''); $params->setCertificate($this->getNewCert($params->getPassword())); - $fileSigned = $this->service->sign($params); - $this->assertNotNull($fileSigned); + $params->setPathPdfSigned('vfs://download/temp'); + $signedFilePath = $params->getTempPdfSignedPath(); + file_put_contents($signedFilePath, 'signed file content'); + $fileSignedContent = $this->service->sign($params); + $this->assertEquals('signed file content', $fileSignedContent); } #[DataProvider('providerSignUsingDifferentPasswords')] public function testSignUsingDifferentPasswords(string $password): void { global $mockExec; - if (!class_exists('JSignPDF\JSignPDFBin\JavaCommandService')) { - $this->markTestSkipped('Install jsignpdf/jsignpdf-bin'); - } + $mockExec = ['Finished: Signature succesfully created.']; $params = JSignParamBuilder::instance()->withDefault(); + vfsStream::setup('download'); + mkdir('vfs://download/jvava/bin', 0755, true); + touch('vfs://download/jvava/bin/java'); + chmod('vfs://download/jvava/bin/java', 0755); + $params->setJavaPath('vfs://download/jvava/bin/java'); + $params->setJavaDownloadUrl(''); + mkdir('vfs://download/jsignpdf', 0755, true); + $params->setjSignPdfJarPath('vfs://download/jsignpdf'); + $params->setJSignPdfDownloadUrl(''); $params->setCertificate($this->getNewCert($password)); $params->setPassword($password); - $mockExec = 'Finished: Signature succesfully created.'; - $path = $params->getTempPdfSignedPath(); - file_put_contents($path, 'dummy'); - $fileSigned = $this->service->sign($params); - $this->assertNotNull($fileSigned); + $params->setPathPdfSigned('vfs://download/temp'); + $signedFilePath = $params->getTempPdfSignedPath(); + file_put_contents($signedFilePath, 'signed file content'); + $fileSignedContent = $this->service->sign($params); + $this->assertEquals('signed file content', $fileSignedContent); } public static function providerSignUsingDifferentPasswords(): array @@ -105,13 +113,22 @@ public static function providerSignUsingDifferentPasswords(): array public function testCertificateExpired() { - if (!class_exists('JSignPDF\JSignPDFBin\JavaCommandService')) { - $this->markTestSkipped('Install jsignpdf/jsignpdf-bin'); - } $this->expectExceptionMessage('Certificate expired.'); $params = JSignParamBuilder::instance()->withDefault(); - $fileSigned = $this->service->sign($params); - $this->assertNotNull($fileSigned); + vfsStream::setup('download'); + mkdir('vfs://download/jvava/bin', 0755, true); + touch('vfs://download/jvava/bin/java'); + chmod('vfs://download/jvava/bin/java', 0755); + $params->setJavaPath('vfs://download/jvava/bin/java'); + $params->setJavaDownloadUrl(''); + mkdir('vfs://download/jsignpdf', 0755, true); + $params->setjSignPdfJarPath('vfs://download/jsignpdf'); + $params->setJSignPdfDownloadUrl(''); + $params->setCertificate($this->getNewCert('123', 0)); + $params->setPassword('123'); + $signedFilePath = $params->getTempPdfSignedPath(); + file_put_contents($signedFilePath, 'signed file content'); + $this->service->sign($params); } public function testSignError() @@ -124,13 +141,25 @@ public function testSignError() public function testWithWhenResponseIsBase64() { - if (!class_exists('JSignPDF\JSignPDFBin\JavaCommandService')) { - $this->markTestSkipped('Install jsignpdf/jsignpdf-bin'); - } - $params = JSignParamBuilder::instance()->withDefault()->setIsOutputTypeBase64(true); - $params->setCertificate($this->getNewCert($params->getPassword())); - $fileSigned = $this->service->sign($params); - $this->assertTrue(base64_decode($fileSigned, true) == true); + global $mockExec; + $mockExec = ['Finished: Signature succesfully created.']; + $params = JSignParamBuilder::instance()->withDefault(); + vfsStream::setup('download'); + mkdir('vfs://download/jvava/bin', 0755, true); + touch('vfs://download/jvava/bin/java'); + chmod('vfs://download/jvava/bin/java', 0755); + $params->setJavaPath('vfs://download/jvava/bin/java'); + $params->setJavaDownloadUrl(''); + mkdir('vfs://download/jsignpdf', 0755, true); + $params->setjSignPdfJarPath('vfs://download/jsignpdf'); + $params->setJSignPdfDownloadUrl(''); + $params->setCertificate($this->getNewCert('123')); + $params->setPassword('123'); + $signedFilePath = $params->getTempPdfSignedPath(); + file_put_contents($signedFilePath, 'signed file content'); + $params->setIsOutputTypeBase64(true); + $signedContent = $this->service->sign($params); + $this->assertEquals(base64_encode('signed file content'), $signedContent); } public function testSignWhenCertificateIsNull() @@ -171,30 +200,30 @@ public function testSignWhenPasswordIsInvalid() public function testJSignPDFNotFound() { $this->expectExceptionMessageMatches('/JSignPDF not found/'); - $params = JSignParamBuilder::instance()->withDefault()->setjSignPdfJarPath('invalid_path'); + $params = JSignParamBuilder::instance()->withDefault(); + $params->setJSignPdfDownloadUrl(''); + $params->setjSignPdfJarPath('invalid_path'); $params->setCertificate($this->getNewCert($params->getPassword())); + $params->setIsUseJavaInstalled(true); $this->service->getVersion($params); } - public function testSignWhenJavaNotFound() - { - $javaVersion = exec("java -version 2>&1"); - $hasJavaVersion = strpos($javaVersion, 'not found') === false; - if ($hasJavaVersion) { - $this->markTestSkipped('Java is already installed, impossible to test if it is not installed'); - } - $this->expectExceptionMessage('Java not installed, set the flag "isUseJavaInstalled" as false or install java.'); - $params = JSignParamBuilder::instance()->withDefault()->setIsUseJavaInstalled(true); - $params->setCertificate($this->getNewCert($params->getPassword())); - $this->service->sign($params); - } - public function testGetVersion() { - if (!class_exists('JSignPDF\JSignPDFBin\JavaCommandService')) { - $this->markTestSkipped('Install jsignpdf/jsignpdf-bin'); - } + global $mockExec; + $mockExec = ['JSignPdf version 2.3.0']; + $params = JSignParamBuilder::instance()->withDefault(); + vfsStream::setup('download'); + mkdir('vfs://download/bin'); + touch('vfs://download/bin/java'); + chmod('vfs://download/bin/java', 0755); + mkdir('vfs://download/jsignpdf_fake_path/'); + touch('vfs://download/jsignpdf_fake_path/.jsignpdf_version_fake_url'); + $params->setJavaPath('vfs://download/bin/java'); + $params->setJSignPdfDownloadUrl('fake_url'); + $params->setIsUseJavaInstalled(true); + $params->setjSignPdfJarPath('vfs://download/jsignpdf_fake_path'); $version = $this->service->getVersion($params); $this->assertNotEmpty($version); } diff --git a/tests/Runtime/JavaRuntimeServiceTest.php b/tests/Runtime/JavaRuntimeServiceTest.php new file mode 100644 index 0000000..a20d13b --- /dev/null +++ b/tests/Runtime/JavaRuntimeServiceTest.php @@ -0,0 +1,223 @@ +testTmpDir = sys_get_temp_dir() . '/jsignpdf_temp_dir_' . uniqid(); + mkdir(directory: $this->testTmpDir, recursive: true); + } + + public function testGetPathWhenJavaIsInstalled(): void { + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + $jsignParam->setIsUseJavaInstalled(true); + $path = $service->getPath($jsignParam); + $this->assertEquals('java', $path); + } + + public function testGetPathWithCustomAndValidJavaPath(): void { + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + vfsStream::setup('download'); + mkdir('vfs://download/bin'); + touch('vfs://download/bin/java'); + chmod('vfs://download/bin/java', 0755); + $jsignParam->setJavaPath('vfs://download/bin/java'); + $jsignParam->setJavaDownloadUrl(''); + $path = $service->getPath($jsignParam); + $this->assertEquals('vfs://download/bin/java', $path); + } + + public function testGetPathWithCustomAndInvalidJavaPath(): void { + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + $jsignParam->setJavaPath(__FILE__); + $jsignParam->setJavaDownloadUrl(''); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/not executable/'); + $service->getPath($jsignParam); + } + + public function testGetPathWithDownloadUrlAndNotRealDirectory(): void { + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + $root = vfsStream::setup('download'); + $root->chmod(0770) + ->chgrp(vfsStream::GROUP_USER_1) + ->chown(vfsStream::OWNER_USER_1); + chgrp('vfs://download', 44); + $jsignParam->setJavaPath('vfs://download/not_real_directory'); + $jsignParam->setJavaDownloadUrl('https://fake.url'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/not a real directory/'); + $service->getPath($jsignParam); + } + + public function testGetPathWithDownloadUrlWithInvalidUrl(): void { + vfsStream::setup('download'); + mkdir('vfs://download/bin'); + touch('vfs://download/bin/java'); + + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + $jsignParam->setJavaPath('vfs://download/bin/java'); + $jsignParam->setJavaDownloadUrl('invalid_url'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/url.*invalid/'); + $service->getPath($jsignParam); + } + + public function testGetPathWithDownloadUrlWith4xxError(): void { + vfsStream::setup('download'); + mkdir('vfs://download/bin'); + touch('vfs://download/bin/java'); + + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + $jsignParam->setJavaPath('vfs://download/bin/java'); + $jsignParam->setJavaDownloadUrl('https://404.domain'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/Failure to download/'); + $service->getPath($jsignParam); + } + + public function testGetPathWithDownloadUrlWithInvalidGzipedFile(): void { + vfsStream::setup('download'); + mkdir('vfs://download/bin'); + touch('vfs://download/bin/java'); + + + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + $jsignParam->setJavaPath('vfs://download/bin/java'); + + $server = new MockWebServer(); + $server->start(); + $server->setResponseOfPath( + '/', + new Response( + 'invalid body response', + ) + ); + $url = $server->getServerRoot(); + $jsignParam->setJavaDownloadUrl($url); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/cannot be extracted/'); + $service->getPath($jsignParam); + } + + public function testGetPathWithDownloadUrlWithInvalidJavaPackage(): void { + mkdir($this->testTmpDir . '/bin', 0755, true); + + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + $jsignParam->setJavaPath($this->testTmpDir . '/bin/java'); + + $tar = new PharData($this->testTmpDir . '/temp.tar.gz'); + $tar->addFromString('file.txt', 'invalid file'); + + $server = new MockWebServer(); + $server->start(); + $server->setResponseOfPath( + '/', + new Response( + gzencode(file_get_contents($this->testTmpDir . '/temp.tar.gz')), + ) + ); + unlink($this->testTmpDir . '/temp.tar.gz'); + $url = $server->getServerRoot(); + $jsignParam->setJavaDownloadUrl($url); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/Invalid tar content/'); + $service->getPath($jsignParam); + } + + public function testGetPathWithDownloadUrlWithInvalidVersion(): void { + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + + // When the version is invalid, will try to download the package + $server = new MockWebServer(); + $server->start(); + $server->setResponseOfPath( + '/', + new Response('invalid response'), + ); + $baseUrl = $server->getServerRoot(); + $url = $baseUrl . '/OpenJDK21U-jre_x64_linux_hotspot_21.0.8_9.tar.gz'; + $jsignParam->setJavaDownloadUrl($url); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/cannot be extracted/'); + $service->getPath($jsignParam); + } + + public function testGetPathWithDownloadUrlWithValidVersion(): void { + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + + $tarGzFilename = 'OpenJDK21U-jre_x64_linux_hotspot_21.0.8_9.tar.gz'; + $url = 'https://fake.url/' . $tarGzFilename; + $jsignParam->setJavaDownloadUrl($url); + + // When have a file with an expected name, will consider that the + // downloaded java version is right + touch($this->testTmpDir . '/.java_version_' . $tarGzFilename); + $jsignParam->setJavaPath($this->testTmpDir . '/bin/java'); + + $javaPath = $service->getPath($jsignParam); + $this->assertEquals($jsignParam->getJavaPath(), $javaPath); + } + + public function testGetPathWithoutJavaFallback(): void { + mkdir($this->testTmpDir . '/bin', 0755, true); + + $jsignParam = new JSignParam(); + $service = new JavaRuntimeService(); + + $jsignParam->setJavaPath(''); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/Java not found/'); + $service->getPath($jsignParam); + } + + protected function tearDown(): void + { + $dirs = glob(sys_get_temp_dir() . '/jsignpdf_temp_*', GLOB_ONLYDIR); + + foreach ($dirs as $dir) { + if (is_dir($dir)) { + $this->removeDirectoryContents($dir); + rmdir($dir); + } + } + } + + private function removeDirectoryContents($dir): void + { + $it = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS); + $files = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($files as $file) { + if ($file->isDir()) { + rmdir($file->getRealPath()); + } else { + unlink($file->getRealPath()); + } + } + } +} diff --git a/vendor-bin/phpunit/composer.json b/vendor-bin/phpunit/composer.json index b5f041e..394a71f 100644 --- a/vendor-bin/phpunit/composer.json +++ b/vendor-bin/phpunit/composer.json @@ -5,6 +5,8 @@ } }, "require-dev": { - "phpunit/phpunit": "^10.5" + "phpunit/phpunit": "^10.5", + "mikey179/vfsstream": "^1.6", + "donatj/mock-webserver": "^2.8" } } diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index d7598c1..1bb837c 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -4,9 +4,137 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fb78960ff7e774a72d424270c4bd3d90", + "content-hash": "a599f9439564eae0c324bd8f816485da", "packages": [], "packages-dev": [ + { + "name": "donatj/mock-webserver", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/donatj/mock-webserver.git", + "reference": "73b5d53a8f1285674ff36b2474a4cb9132d81177" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/donatj/mock-webserver/zipball/73b5d53a8f1285674ff36b2474a4cb9132d81177", + "reference": "73b5d53a8f1285674ff36b2474a4cb9132d81177", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-sockets": "*", + "php": ">=7.1", + "ralouphie/getallheaders": "~2.0 || ~3.0" + }, + "require-dev": { + "corpus/coding-standard": "^0.6.0 || ^0.9.0", + "donatj/drop": "^1.0", + "ext-curl": "*", + "friendsofphp/php-cs-fixer": "^3.1", + "phpunit/phpunit": "~7|~9", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "donatj\\MockWebServer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jesse G. Donat", + "email": "donatj@gmail.com", + "homepage": "https://donatstudios.com", + "role": "Lead" + } + ], + "description": "Simple mock web server for unit testing", + "keywords": [ + "dev", + "http", + "mock", + "phpunit", + "testing", + "unit testing", + "webserver" + ], + "support": { + "issues": "https://github.com/donatj/mock-webserver/issues", + "source": "https://github.com/donatj/mock-webserver/tree/v2.8.0" + }, + "funding": [ + { + "url": "https://www.paypal.me/donatj/15", + "type": "custom" + }, + { + "url": "https://github.com/donatj", + "type": "github" + }, + { + "url": "https://ko-fi.com/donatj", + "type": "ko_fi" + } + ], + "time": "2025-06-26T22:24:11+00:00" + }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.12", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5||^8.5||^9.6", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2024-08-29T18:43:31+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -673,6 +801,50 @@ ], "time": "2025-07-11T04:07:17+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.1",