generated from jric11/baseProject
1537 lines
58 KiB
PHP
1537 lines
58 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
/**
|
||
|
|
* TcpdfTest.php
|
||
|
|
*
|
||
|
|
* @since 2002-08-03
|
||
|
|
* @category Library
|
||
|
|
* @package Pdf
|
||
|
|
* @author Nicola Asuni <info@tecnick.com>
|
||
|
|
* @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
|
||
|
|
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||
|
|
* @link https://github.com/tecnickcom/tc-lib-pdf
|
||
|
|
*
|
||
|
|
* This file is part of tc-lib-pdf software library.
|
||
|
|
*/
|
||
|
|
|
||
|
|
namespace Test;
|
||
|
|
|
||
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Tcpdf Pdf class test
|
||
|
|
*
|
||
|
|
* @since 2002-08-03
|
||
|
|
* @category Library
|
||
|
|
* @package Pdf
|
||
|
|
* @author Nicola Asuni <info@tecnick.com>
|
||
|
|
* @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
|
||
|
|
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
||
|
|
* @link https://github.com/tecnickcom/tc-lib-pdf
|
||
|
|
*/
|
||
|
|
class TcpdfTest extends TestUtil
|
||
|
|
{
|
||
|
|
/** @throws \Throwable */
|
||
|
|
protected function getTestObject(): \Com\Tecnick\Pdf\Tcpdf
|
||
|
|
{
|
||
|
|
return new \Com\Tecnick\Pdf\Tcpdf();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return array{pid: int}
|
||
|
|
* @throws \Throwable
|
||
|
|
*/
|
||
|
|
private function initFontAndAddRawPage(\Com\Tecnick\Pdf\Tcpdf $obj): array
|
||
|
|
{
|
||
|
|
/** @var \Com\Tecnick\Pdf\Font\Stack $font */
|
||
|
|
$font = $this->getObjectProperty($obj, 'font');
|
||
|
|
/** @var int $pon */
|
||
|
|
$pon = $this->getObjectProperty($obj, 'pon');
|
||
|
|
$fontfile = (string) \realpath(__DIR__
|
||
|
|
. '/../vendor/tecnickcom/tc-lib-pdf-font/target/fonts/core/helvetica.json');
|
||
|
|
$font->insert($pon, 'helvetica', '', 10, null, null, $fontfile);
|
||
|
|
|
||
|
|
/** @var \Com\Tecnick\Pdf\Page\Page $page */
|
||
|
|
$page = $this->getObjectProperty($obj, 'page');
|
||
|
|
/** @var array<string, mixed> $rawPage */
|
||
|
|
$rawPage = $page->add([]);
|
||
|
|
if (!isset($rawPage['pid']) || !\is_int($rawPage['pid'])) {
|
||
|
|
$this->fail('Unexpected page id type from add([]).');
|
||
|
|
}
|
||
|
|
$pid = $rawPage['pid'];
|
||
|
|
|
||
|
|
return ['pid' => $pid];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameAcceptsValidPdfName(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setPDFFilename('my_test_file.pdf');
|
||
|
|
|
||
|
|
$this->assertSame('my_test_file.pdf', $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
$this->assertSame('my_test_file.pdf', $this->getObjectProperty($obj, 'encpdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameAcceptsUnicodeName(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setPDFFilename('Resume 日本語.pdf');
|
||
|
|
|
||
|
|
$this->assertSame('Resume 日本語.pdf', $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
$this->assertSame('Resume%20%E6%97%A5%E6%9C%AC%E8%AA%9E.pdf', $this->getObjectProperty($obj, 'encpdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameNormalizesUnicodeToNfcWhenAvailable(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setPDFFilename("Cafe\u{0301}.pdf");
|
||
|
|
|
||
|
|
$expectedName = \class_exists('\\Normalizer') ? 'Café.pdf' : "Cafe\u{0301}.pdf";
|
||
|
|
$expectedEncoded = \rawurlencode($expectedName);
|
||
|
|
|
||
|
|
$this->assertSame($expectedName, $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
$this->assertSame($expectedEncoded, $this->getObjectProperty($obj, 'encpdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameRejectsInvalidExtension(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$before = (string) $this->getObjectProperty($obj, 'pdffilename');
|
||
|
|
|
||
|
|
$obj->setPDFFilename('bad-name.txt');
|
||
|
|
|
||
|
|
$this->assertSame($before, $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameRejectsControlCharacters(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$before = (string) $this->getObjectProperty($obj, 'pdffilename');
|
||
|
|
|
||
|
|
$obj->setPDFFilename("bad\tname.pdf");
|
||
|
|
|
||
|
|
$this->assertSame($before, $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameRejectsMarkOnlyStem(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$before = (string) $this->getObjectProperty($obj, 'pdffilename');
|
||
|
|
|
||
|
|
$obj->setPDFFilename("\u{0301}.pdf");
|
||
|
|
|
||
|
|
$this->assertSame($before, $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameRejectsDetachedMarkAfterSeparator(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$before = (string) $this->getObjectProperty($obj, 'pdffilename');
|
||
|
|
|
||
|
|
$obj->setPDFFilename("a \u{0301}.pdf");
|
||
|
|
|
||
|
|
$this->assertSame($before, $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameRejectsNamesLongerThan255Bytes(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$before = (string) $this->getObjectProperty($obj, 'pdffilename');
|
||
|
|
|
||
|
|
$obj->setPDFFilename(\str_repeat('a', 252) . '.pdf');
|
||
|
|
|
||
|
|
$this->assertSame($before, $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetPDFFilenameAcceptsNamesUpTo255Bytes(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$name = \str_repeat('a', 251) . '.pdf';
|
||
|
|
|
||
|
|
$obj->setPDFFilename($name);
|
||
|
|
|
||
|
|
$this->assertSame($name, $this->getObjectProperty($obj, 'pdffilename'));
|
||
|
|
$this->assertSame($name, $this->getObjectProperty($obj, 'encpdffilename'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSpaceRegexpParsesPatternAndModifiers(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setSpaceRegexp('/abc/i');
|
||
|
|
|
||
|
|
/** @var array{r: string, p: string, m: string} $regexp */
|
||
|
|
$regexp = $this->getObjectProperty($obj, 'spaceregexp');
|
||
|
|
$this->assertSame('/abc/i', $regexp['r']);
|
||
|
|
$this->assertSame('abc', $regexp['p']);
|
||
|
|
$this->assertSame('i', $regexp['m']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testConstructorAlignsFileIdWithInjectedEncryptionObject(): void
|
||
|
|
{
|
||
|
|
$fileid = \md5('tcpdf-encryption-fileid');
|
||
|
|
$enc = new \Com\Tecnick\Pdf\Encrypt\Encrypt(true, $fileid, 2, ['modify', 'copy'], 'demo-user', 'demo-owner');
|
||
|
|
|
||
|
|
$obj = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, '', $enc);
|
||
|
|
|
||
|
|
$this->assertSame($fileid, $this->getObjectProperty($obj, 'fileid'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testConstructorPassesFileOptionsToSharedFileHelper(): void
|
||
|
|
{
|
||
|
|
$defaultCurlOpts = [
|
||
|
|
CURLOPT_TIMEOUT => 12,
|
||
|
|
CURLOPT_USERAGENT => 'tc-lib-pdf-test',
|
||
|
|
];
|
||
|
|
$fixedCurlOpts = [
|
||
|
|
CURLOPT_SSL_VERIFYHOST => 2,
|
||
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
||
|
|
];
|
||
|
|
$allowedHosts = ['localhost', 'example.test'];
|
||
|
|
|
||
|
|
$obj = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, '', null, [
|
||
|
|
'defaultCurlOpts' => $defaultCurlOpts,
|
||
|
|
'fixedCurlOpts' => $fixedCurlOpts,
|
||
|
|
'allowedHosts' => $allowedHosts,
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var \Com\Tecnick\File\File $file */
|
||
|
|
$file = $this->getObjectProperty($obj, 'file');
|
||
|
|
/** @var \Com\Tecnick\Pdf\Image\Import $image */
|
||
|
|
$image = $this->getObjectProperty($obj, 'image');
|
||
|
|
/** @var \Com\Tecnick\File\File $imageFile */
|
||
|
|
$imageFile = $this->getObjectProperty($image, 'fileHelper');
|
||
|
|
|
||
|
|
$this->assertSame($allowedHosts, $this->getObjectProperty($file, 'allowedHosts'));
|
||
|
|
$this->assertSame($defaultCurlOpts, $this->getObjectProperty($file, 'defaultCurlOpts'));
|
||
|
|
$this->assertSame($fixedCurlOpts, $this->getObjectProperty($file, 'fixedCurlOpts'));
|
||
|
|
$this->assertSame($allowedHosts, $this->getObjectProperty($imageFile, 'allowedHosts'));
|
||
|
|
$this->assertSame($defaultCurlOpts, $this->getObjectProperty($imageFile, 'defaultCurlOpts'));
|
||
|
|
$this->assertSame($fixedCurlOpts, $this->getObjectProperty($imageFile, 'fixedCurlOpts'));
|
||
|
|
|
||
|
|
/** @var array<string> $allowedPaths */
|
||
|
|
$allowedPaths = $this->getObjectProperty($file, 'allowedPaths');
|
||
|
|
$this->assertContains((string) \realpath(__DIR__ . '/..'), $allowedPaths);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testConstructorUsesCustomAllowedPathsWithoutMergingDefaults(): void
|
||
|
|
{
|
||
|
|
$customDir = (string) \realpath(__DIR__ . '/fixtures');
|
||
|
|
$obj = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, '', null, [
|
||
|
|
'allowedPaths' => [$customDir],
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var \Com\Tecnick\File\File $file */
|
||
|
|
$file = $this->getObjectProperty($obj, 'file');
|
||
|
|
/** @var array<string> $allowedPaths */
|
||
|
|
$allowedPaths = $this->getObjectProperty($file, 'allowedPaths');
|
||
|
|
|
||
|
|
$this->assertNotContains((string) \realpath(__DIR__ . '/..'), $allowedPaths);
|
||
|
|
$this->assertContains($customDir, $allowedPaths);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testConstructorUsesMarkupAllowedPathsFallbackAndOverride(): void
|
||
|
|
{
|
||
|
|
$customDir = (string) \realpath(__DIR__ . '/fixtures');
|
||
|
|
$markupDir = (string) \realpath(__DIR__ . '/..');
|
||
|
|
|
||
|
|
$fallback = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, '', null, [
|
||
|
|
'allowedPaths' => [$customDir],
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var \Com\Tecnick\File\File $fallbackMarkupFile */
|
||
|
|
$fallbackMarkupFile = $this->getObjectProperty($fallback, 'markupFile');
|
||
|
|
/** @var array<string> $fallbackMarkupPaths */
|
||
|
|
$fallbackMarkupPaths = $this->getObjectProperty($fallbackMarkupFile, 'allowedPaths');
|
||
|
|
$this->assertSame([$customDir], $fallbackMarkupPaths);
|
||
|
|
|
||
|
|
$override = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, '', null, [
|
||
|
|
'allowedPaths' => [$customDir],
|
||
|
|
'markupAllowedPaths' => [$markupDir],
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var \Com\Tecnick\File\File $overrideMarkupFile */
|
||
|
|
$overrideMarkupFile = $this->getObjectProperty($override, 'markupFile');
|
||
|
|
/** @var array<string> $overrideMarkupPaths */
|
||
|
|
$overrideMarkupPaths = $this->getObjectProperty($overrideMarkupFile, 'allowedPaths');
|
||
|
|
$this->assertSame([$markupDir], $overrideMarkupPaths);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
#[DataProvider('displayModeFixtureProvider')]
|
||
|
|
public function testSetDisplayModeStoresExpectedZoom(string|int $inputZoom, string|int $expectedZoom): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$ret = $obj->setDisplayMode($inputZoom);
|
||
|
|
|
||
|
|
/** @var array{zoom: string|int|float} $display */
|
||
|
|
$display = $this->getObjectProperty($obj, 'display');
|
||
|
|
$this->assertSame($obj, $ret);
|
||
|
|
$this->assertSame($expectedZoom, $display['zoom']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetUserRightsMergesValues(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setUserRights([
|
||
|
|
'annots' => '/All',
|
||
|
|
'document' => '/FullSave',
|
||
|
|
'ef' => '/All',
|
||
|
|
'enabled' => true,
|
||
|
|
'form' => '/All',
|
||
|
|
'formex' => '/All',
|
||
|
|
'signature' => '/All',
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var array{enabled: bool, document: string} $rights */
|
||
|
|
$rights = $this->getObjectProperty($obj, 'userrights');
|
||
|
|
$this->assertTrue($rights['enabled']);
|
||
|
|
$this->assertSame('/FullSave', $rights['document']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array<string, array{0: string|int, 1: string|int}> */
|
||
|
|
public static function displayModeFixtureProvider(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'invalid_token_uses_default' => ['invalid-zoom-token', 'default'],
|
||
|
|
'numeric_zoom_is_preserved' => [125, 125],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignTimeStampThrowsWhenEnabledWithoutHost(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$obj->setSignTimeStamp([
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => '',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha256',
|
||
|
|
'policy_oid' => '',
|
||
|
|
'nonce_enabled' => true,
|
||
|
|
'timeout' => 5,
|
||
|
|
'verify_peer' => true,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignTimeStampStoresDataWhenHostProvided(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setSignTimeStamp([
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => 'https://tsa.example.test',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha512',
|
||
|
|
'policy_oid' => '1.2.3.4',
|
||
|
|
'nonce_enabled' => false,
|
||
|
|
'timeout' => 9,
|
||
|
|
'verify_peer' => false,
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var array{enabled: bool, host: string, hash_algorithm: string, timeout: int, verify_peer: bool} $timeStamp */
|
||
|
|
$timeStamp = $this->getObjectProperty($obj, 'sigtimestamp');
|
||
|
|
$this->assertTrue($timeStamp['enabled']);
|
||
|
|
$this->assertSame('https://tsa.example.test', $timeStamp['host']);
|
||
|
|
$this->assertSame('sha512', $timeStamp['hash_algorithm']);
|
||
|
|
$this->assertSame(9, $timeStamp['timeout']);
|
||
|
|
$this->assertFalse($timeStamp['verify_peer']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignTimeStampThrowsOnInvalidHashAlgorithm(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$obj->setSignTimeStamp([
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => 'https://tsa.example.test',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha1',
|
||
|
|
'policy_oid' => '',
|
||
|
|
'nonce_enabled' => true,
|
||
|
|
'timeout' => 5,
|
||
|
|
'verify_peer' => true,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureStoresLtvOptions(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
|
||
|
|
$obj->setSignature([
|
||
|
|
'appearance' => ['empty' => [], 'name' => '', 'page' => 0, 'rect' => ''],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 0,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => 'dummy-signcert',
|
||
|
|
'ltv' => [
|
||
|
|
'enabled' => true,
|
||
|
|
'embed_ocsp' => false,
|
||
|
|
'embed_crl' => false,
|
||
|
|
'embed_certs' => true,
|
||
|
|
'include_dss' => true,
|
||
|
|
'include_vri' => true,
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var array{ltv: array{enabled: bool, embed_ocsp: bool, embed_crl: bool}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertTrue($signature['ltv']['enabled']);
|
||
|
|
$this->assertFalse($signature['ltv']['embed_ocsp']);
|
||
|
|
$this->assertFalse($signature['ltv']['embed_crl']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureThrowsOnMissingSigningCertificate(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$obj->setSignature([
|
||
|
|
'appearance' => ['empty' => [], 'name' => '', 'page' => 0, 'rect' => ''],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 0,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => '',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testGetBarcodeReturnsDrawingCommands(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$out = $obj->getBarcode('C39', 'ABC123');
|
||
|
|
|
||
|
|
$this->assertNotSame('', $out);
|
||
|
|
$this->bcAssertMatchesRegularExpression('/\bre\b/', $out);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testGetBarcodeThrowsOnInvalidType(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Barcode\Exception::class);
|
||
|
|
|
||
|
|
$obj->getBarcode('INVALID_TYPE', 'ABC123');
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureAppearanceStoresPageNameAndRect(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->setSignatureAppearance(10, 20, 30, 15, $page['pid'], 'MainSig');
|
||
|
|
|
||
|
|
/** @var array{appearance: array{page: int, name: string, rect: string}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$appearance = $signature['appearance'];
|
||
|
|
|
||
|
|
$this->assertSame($page['pid'], $appearance['page']);
|
||
|
|
$this->assertSame('MainSig', $appearance['name']);
|
||
|
|
$this->bcAssertMatchesRegularExpression('/^[-0-9.]+\s+[-0-9.]+\s+[-0-9.]+\s+[-0-9.]+$/', $appearance['rect']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testAddEmptySignatureAppearanceAddsEntry(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->addEmptySignatureAppearance(5, 6, 20, 10, $page['pid'], 'EmptySig');
|
||
|
|
|
||
|
|
/** @var array{appearance: array{empty: array<int, array{name: string, page: int, objid: int}>}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertNotEmpty($signature['appearance']['empty']);
|
||
|
|
assert(isset($signature['appearance']['empty'][0]), "\$signature['appearance']['empty'][0] must be set");
|
||
|
|
$entry = $signature['appearance']['empty'][0];
|
||
|
|
$this->assertSame('EmptySig', $entry['name']);
|
||
|
|
$this->assertSame($page['pid'], $entry['page']);
|
||
|
|
$this->assertIsInt($entry['objid']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testAddTOCHandlesEmptyOutlineList(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->addTOC();
|
||
|
|
|
||
|
|
/** @var array<int, array<string, mixed>> $outlines */
|
||
|
|
$outlines = $this->getObjectProperty($obj, 'outlines');
|
||
|
|
$this->assertCount(0, $outlines);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testAddTOCProcessesBookmarks(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->setBookmark('Section 1', '', 0, $page['pid']);
|
||
|
|
$obj->addTOC($page['pid']);
|
||
|
|
|
||
|
|
/** @var array<int, array{t: string, p: int}> $outlines */
|
||
|
|
$outlines = $this->getObjectProperty($obj, 'outlines');
|
||
|
|
$this->assertCount(1, $outlines);
|
||
|
|
assert(isset($outlines[0]), "\$outlines[0] must be set");
|
||
|
|
$this->assertSame('Section 1', $outlines[0]['t']);
|
||
|
|
$this->assertSame($page['pid'], $outlines[0]['p']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testNewLayerReturnsBeginLayerOperatorAndStoresLayer(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$out = $obj->newLayer('Layer 1', ['view' => true], true, true, true);
|
||
|
|
|
||
|
|
/** @var array<int, array{name: string, intent: string}> $layers */
|
||
|
|
$layers = $this->getObjectProperty($obj, 'pdflayer');
|
||
|
|
$this->assertSame(" /OC /LYR001 BDC\n", $out);
|
||
|
|
$this->assertCount(1, $layers);
|
||
|
|
assert(isset($layers[0]), "\$layers[0] must be set");
|
||
|
|
$this->assertSame('Layer1', $layers[0]['name']);
|
||
|
|
$this->assertSame('/View', $layers[0]['intent']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testCloseLayerReturnsEmcOperator(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->assertSame("EMC\n", $obj->closeLayer());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testNewLayerFallsBackToAutoNameAndAddsDesignIntent(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$out = $obj->newLayer('***', ['design' => true], true, true, true);
|
||
|
|
|
||
|
|
/** @var array<int, array{name: string, intent: string}> $layers */
|
||
|
|
$layers = $this->getObjectProperty($obj, 'pdflayer');
|
||
|
|
$this->assertSame(" /OC /LYR001 BDC\n", $out);
|
||
|
|
$this->assertCount(1, $layers);
|
||
|
|
assert(isset($layers[0]), "\$layers[0] must be set");
|
||
|
|
$this->assertSame('LYR001', $layers[0]['name']);
|
||
|
|
$this->assertSame('/Design', $layers[0]['intent']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testConstructorPdfaModesSetExpectedFlags(): void
|
||
|
|
{
|
||
|
|
$pdfa1u = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, 'pdfa1u');
|
||
|
|
$this->assertSame(1, $this->getObjectProperty($pdfa1u, 'pdfa'));
|
||
|
|
$this->assertSame('B', $this->getObjectProperty($pdfa1u, 'pdfaConformance'));
|
||
|
|
|
||
|
|
$pdfa2u = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, 'pdfa2u');
|
||
|
|
$this->assertSame(2, $this->getObjectProperty($pdfa2u, 'pdfa'));
|
||
|
|
$this->assertSame('U', $this->getObjectProperty($pdfa2u, 'pdfaConformance'));
|
||
|
|
|
||
|
|
foreach (['pdfx', 'pdfx1a', 'pdfx3', 'pdfx4', 'pdfx5'] as $mode) {
|
||
|
|
$pdfx = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, $mode);
|
||
|
|
$this->assertTrue($this->getObjectProperty($pdfx, 'pdfx'));
|
||
|
|
$this->assertSame($mode, $this->getObjectProperty($pdfx, 'pdfxMode'));
|
||
|
|
$this->assertSame('', $this->getObjectProperty($pdfx, 'pdfuaMode'));
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (['pdfua', 'pdfua1', 'pdfua2'] as $mode) {
|
||
|
|
$pdfua = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, $mode);
|
||
|
|
$this->assertSame($mode, $this->getObjectProperty($pdfua, 'pdfuaMode'));
|
||
|
|
$this->assertFalse($this->getObjectProperty($pdfua, 'pdfx'));
|
||
|
|
$this->assertSame('', $this->getObjectProperty($pdfua, 'pdfxMode'));
|
||
|
|
}
|
||
|
|
|
||
|
|
$unknown = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, 'pdfunknown');
|
||
|
|
$this->assertFalse($this->getObjectProperty($unknown, 'pdfx'));
|
||
|
|
$this->assertSame('', $this->getObjectProperty($unknown, 'pdfxMode'));
|
||
|
|
$this->assertSame('', $this->getObjectProperty($unknown, 'pdfuaMode'));
|
||
|
|
$this->assertSame(0, $this->getObjectProperty($unknown, 'pdfa'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testConstructorWithUnicodeDisabledSetsAsciiWhitespacePattern(): void
|
||
|
|
{
|
||
|
|
$obj = new \Com\Tecnick\Pdf\Tcpdf('mm', false, false, true);
|
||
|
|
|
||
|
|
/** @var array{r: string} $regexp */
|
||
|
|
$regexp = $this->getObjectProperty($obj, 'spaceregexp');
|
||
|
|
$this->assertSame('/[^\S\xa0]/', $regexp['r']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testPdfxRestrictiveModesForceDeviceCmykProcessColors(): void
|
||
|
|
{
|
||
|
|
foreach (['pdfx', 'pdfx1a', 'pdfx3'] as $mode) {
|
||
|
|
$obj = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, $mode);
|
||
|
|
|
||
|
|
$fill = $obj->color->getPdfColor('#336699');
|
||
|
|
$stroke = $obj->color->getPdfColor('#336699', true);
|
||
|
|
|
||
|
|
$this->assertStringContainsString(" k\n", $fill);
|
||
|
|
$this->assertStringNotContainsString(" rg\n", $fill);
|
||
|
|
$this->assertStringContainsString(" K\n", $stroke);
|
||
|
|
$this->assertStringNotContainsString(" RG\n", $stroke);
|
||
|
|
|
||
|
|
// Spot colors remain spot operators in restrictive PDF/X modes.
|
||
|
|
$spot = $obj->color->getPdfColor('cyan');
|
||
|
|
$this->assertStringContainsString('scn', $spot);
|
||
|
|
$this->assertStringContainsString('/CS', $spot);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testDefaultAndPdfx4KeepRgbProcessColors(): void
|
||
|
|
{
|
||
|
|
$default = new \Com\Tecnick\Pdf\Tcpdf();
|
||
|
|
$pdfx4 = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, 'pdfx4');
|
||
|
|
|
||
|
|
$this->assertStringContainsString(" rg\n", $default->color->getPdfColor('#336699'));
|
||
|
|
$this->assertStringContainsString(" rg\n", $pdfx4->color->getPdfColor('#336699'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testCssColorModelsEmitExpectedPdfOperators(): void
|
||
|
|
{
|
||
|
|
$obj = new \Com\Tecnick\Pdf\Tcpdf();
|
||
|
|
|
||
|
|
$cases = [
|
||
|
|
'named navy' => ['navy', " rg\n"],
|
||
|
|
'gray g()' => ['g(50%)', " g\n"],
|
||
|
|
'hex short' => ['#0f0', " rg\n"],
|
||
|
|
'hex long' => ['#1e90ff', " rg\n"],
|
||
|
|
'rgb' => ['rgb(255, 99, 71)', " rg\n"],
|
||
|
|
'rgba' => ['rgba(30, 144, 255, 0.25)', " rg\n"],
|
||
|
|
'hsl' => ['hsl(120, 100%, 25%)', " rg\n"],
|
||
|
|
'hsla' => ['hsla(300, 100%, 50%, 0.20)', " rg\n"],
|
||
|
|
'lab' => ['lab(54.29% -19.04 38.25)', " scn\n"],
|
||
|
|
'lab alpha' => ['lab(54.29% -19.04 38.25 / 0.35)', " scn\n"],
|
||
|
|
'cmyk' => ['cmyk(67, 33, 0, 25)', " k\n"],
|
||
|
|
'spot function' => ['spot(cyan, 35%)', " scn\n"],
|
||
|
|
];
|
||
|
|
|
||
|
|
foreach ($cases as $label => [$color, $operator]) {
|
||
|
|
$pdfColor = $obj->color->getPdfColor($color);
|
||
|
|
$this->assertTrue($pdfColor !== '', 'Expected non-empty PDF color output for case: ' . $label);
|
||
|
|
$this->assertStringContainsString($operator, $pdfColor, 'Unexpected PDF operator for case: ' . $label);
|
||
|
|
if ($label === 'lab' || $label === 'lab alpha' || $label === 'spot function') {
|
||
|
|
$this->assertStringContainsString('/CS', $pdfColor);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureSetsDefaultPrivkeyAndSignFlag(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$obj->setSignature([
|
||
|
|
'appearance' => [
|
||
|
|
'empty' => [
|
||
|
|
[
|
||
|
|
'objid' => 321,
|
||
|
|
'name' => 'EmptySig',
|
||
|
|
'page' => $page['pid'],
|
||
|
|
'rect' => '0 0 1 1',
|
||
|
|
],
|
||
|
|
],
|
||
|
|
'name' => 'MainSig',
|
||
|
|
'page' => $page['pid'],
|
||
|
|
'rect' => '0 0 10 5',
|
||
|
|
],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => '',
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => 'dummy-signcert',
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var array{privkey: string, signcert: string} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertSame('dummy-signcert', $signature['privkey']);
|
||
|
|
$this->assertTrue($this->getObjectProperty($obj, 'sign'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureForExternalSigningUsesPlaceholderCertificate(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$obj->setSignatureForExternalSigning([
|
||
|
|
'appearance' => [
|
||
|
|
'empty' => [],
|
||
|
|
'name' => 'MainSig',
|
||
|
|
'page' => $page['pid'],
|
||
|
|
'rect' => '0 0 10 5',
|
||
|
|
],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => '',
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var array{signcert: string, privkey: string} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertSame('__external_signing__', $signature['signcert']);
|
||
|
|
$this->assertSame('__external_signing__', $signature['privkey']);
|
||
|
|
$this->assertTrue($this->getObjectProperty($obj, 'sign'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testGetExternalSignaturePreparationReturnsHashAndPreparedPdf(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$obj->setSignatureForExternalSigning([
|
||
|
|
'appearance' => [
|
||
|
|
'empty' => [],
|
||
|
|
'name' => 'MainSig',
|
||
|
|
'page' => $page['pid'],
|
||
|
|
'rect' => '0 0 10 5',
|
||
|
|
],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => '',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$prepared = $obj->getExternalSignaturePreparation('sha256');
|
||
|
|
|
||
|
|
$this->assertSame('sha256', $prepared['algorithm']);
|
||
|
|
$this->assertCount(4, $prepared['byte_range']);
|
||
|
|
$this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $prepared['hash_hex']);
|
||
|
|
$this->assertNotSame('', $prepared['prepared_pdf']);
|
||
|
|
$this->assertStringContainsString('/ByteRange[0 ', $prepared['prepared_pdf']);
|
||
|
|
$this->assertStringNotContainsString('**********', $prepared['prepared_pdf']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testApplyExternalSignatureInjectsProvidedCmsData(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$obj->setSignatureForExternalSigning([
|
||
|
|
'appearance' => [
|
||
|
|
'empty' => [],
|
||
|
|
'name' => 'MainSig',
|
||
|
|
'page' => $page['pid'],
|
||
|
|
'rect' => '0 0 10 5',
|
||
|
|
],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => '',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$prepared = $obj->getExternalSignaturePreparation('sha256');
|
||
|
|
$signedPdf = $obj->applyExternalSignature($prepared['prepared_pdf'], $prepared['byte_range'], 'ABC', 'binary');
|
||
|
|
|
||
|
|
$this->assertStringContainsString('/Contents<414243', $signedPdf);
|
||
|
|
$this->assertMatchesRegularExpression('#/ByteRange\[0 \d+ \d+ \d+\]#', $signedPdf);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignAnnotRefsReturnsWhenNoEmptySignaturesAreDefined(): void
|
||
|
|
{
|
||
|
|
$obj = new TestableTcpdf();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$this->setObjectProperty($obj, 'objid', ['signature' => 123]);
|
||
|
|
$this->setObjectProperty($obj, 'signature', [
|
||
|
|
'appearance' => [
|
||
|
|
'empty' => [],
|
||
|
|
'name' => 'MainSig',
|
||
|
|
'page' => $page['pid'],
|
||
|
|
'rect' => '0 0 10 10',
|
||
|
|
],
|
||
|
|
'approval' => false,
|
||
|
|
'cert_type' => 0,
|
||
|
|
'extracerts' => '',
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => '',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$obj->exposeSetSignAnnotRefs();
|
||
|
|
|
||
|
|
/** @var array{appearance: array{empty: array<int, mixed>, page: int}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertSame([], $signature['appearance']['empty']);
|
||
|
|
$this->assertSame($page['pid'], $signature['appearance']['page']);
|
||
|
|
|
||
|
|
/** @var array{annotrefs: array<int>} $pageData */
|
||
|
|
$pageData = $obj->page->getPage($page['pid']);
|
||
|
|
$this->assertContains(123, $pageData['annotrefs']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureAppearanceAddsMainSignatureAnnotationReference(): void
|
||
|
|
{
|
||
|
|
$obj = new TestableTcpdf();
|
||
|
|
$this->initFontAndAddRawPage($obj);
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$this->setObjectProperty($obj, 'objid', ['signature' => 321]);
|
||
|
|
$obj->setSignatureAppearance(1, 2, 3, 4, $page['pid'], 'MainSig');
|
||
|
|
|
||
|
|
/** @var array{appearance: array{page: int, name: string}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertSame($page['pid'], $signature['appearance']['page']);
|
||
|
|
$this->assertSame('MainSig', $signature['appearance']['name']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testGetSignatureObjectIDReturnsReservedObjectId(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$certPath = __DIR__ . '/../examples/data/cert/tcpdf.crt';
|
||
|
|
$certPem = (string) \file_get_contents($certPath);
|
||
|
|
|
||
|
|
$obj->setSignature([
|
||
|
|
'appearance' => [
|
||
|
|
'empty' => [],
|
||
|
|
'name' => 'MainSig',
|
||
|
|
'page' => $page['pid'],
|
||
|
|
'rect' => '0 0 10 5',
|
||
|
|
],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => $certPem,
|
||
|
|
'signcert' => $certPem,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$this->assertGreaterThan(0, $obj->getSignatureObjectID());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureAppearanceStreamStoresModeAndState(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setSignatureAppearanceStream('q 0 g Q', 'N', 'On');
|
||
|
|
|
||
|
|
/** @var array{appearance: array{ap: array<string, array<string, string>>, as: string}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$appearance = $signature['appearance'];
|
||
|
|
$ap = $appearance['ap'];
|
||
|
|
$normal = $ap['n'] ?? [];
|
||
|
|
$this->assertSame('q 0 g Q', $normal['On'] ?? null);
|
||
|
|
$this->assertSame('On', $signature['appearance']['as']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureAppearanceXObjectStoresXObjectId(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$obj->setSignatureAppearanceXObject('IMP1');
|
||
|
|
|
||
|
|
/** @var array{appearance: array{xobj: string}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertSame('IMP1', $signature['appearance']['xobj']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testEnableSignatureApprovalTogglesFlag(): void
|
||
|
|
{
|
||
|
|
$obj = new TestableTcpdf();
|
||
|
|
|
||
|
|
$ret = $obj->exposeEnableSignatureApproval(true);
|
||
|
|
$this->assertSame($obj, $ret);
|
||
|
|
$this->assertTrue($this->getObjectProperty($obj, 'sigapp'));
|
||
|
|
|
||
|
|
$obj->exposeEnableSignatureApproval(false);
|
||
|
|
$this->assertFalse($this->getObjectProperty($obj, 'sigapp'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testAddTOCSupportsRtlPositioningAndBookmarkColor(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->setBookmark('RTL entry', '', 0, $page['pid'], 0, 0, '', 'red');
|
||
|
|
|
||
|
|
$obj->addTOC($page['pid'], 0, 0, -1, true);
|
||
|
|
|
||
|
|
/** @var array<int, array{t: string}> $outlines */
|
||
|
|
$outlines = $this->getObjectProperty($obj, 'outlines');
|
||
|
|
$this->assertCount(1, $outlines);
|
||
|
|
assert(isset($outlines[0]), "\$outlines[0] must be set");
|
||
|
|
$this->assertSame('RTL entry', $outlines[0]['t']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testAddTOCHandlesRegionOverflowAndPageTransitions(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->setBookmark(\str_repeat('Long TOC line ', 250), '', 0, $page['pid']);
|
||
|
|
|
||
|
|
$obj->addTOC($page['pid'], 0, 10000, 20, false);
|
||
|
|
|
||
|
|
/** @var array<int, array{t: string}> $outlines */
|
||
|
|
$outlines = $this->getObjectProperty($obj, 'outlines');
|
||
|
|
$this->assertCount(1, $outlines);
|
||
|
|
assert(isset($outlines[0]), "\$outlines[0] must be set");
|
||
|
|
$this->assertStringStartsWith('Long TOC line', $outlines[0]['t']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testLanguageSettersUpdateLanguageMetadata(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
|
||
|
|
$obj->setLanguageArray(['a_meta_language' => 'it-IT', 'custom' => 'x']);
|
||
|
|
/** @var array<string, string> $lang */
|
||
|
|
$lang = $this->getObjectProperty($obj, 'lang');
|
||
|
|
$this->assertSame('it-IT', $lang['a_meta_language'] ?? null);
|
||
|
|
$this->assertSame('x', $lang['custom'] ?? null);
|
||
|
|
|
||
|
|
$obj->setLanguage('en-US');
|
||
|
|
/** @var array<string, string> $updated */
|
||
|
|
$updated = $this->getObjectProperty($obj, 'lang');
|
||
|
|
$this->assertSame('en-US', $updated['a_meta_language'] ?? null);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureAddsDefaultLtvOptionsWhenMissing(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
|
||
|
|
/** @var array<string, mixed> $signatureState */
|
||
|
|
$signatureState = $this->getObjectProperty($obj, 'signature');
|
||
|
|
unset($signatureState['ltv']);
|
||
|
|
$this->setObjectProperty($obj, 'signature', $signatureState);
|
||
|
|
|
||
|
|
$obj->setSignature([
|
||
|
|
'appearance' => ['empty' => [], 'name' => '', 'page' => 0, 'rect' => ''],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 0,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => 'dummy-signcert',
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var array{ltv: array{enabled: bool, embed_ocsp: bool, embed_crl: bool, embed_certs: bool, include_dss: bool, include_vri: bool}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertFalse($signature['ltv']['enabled']);
|
||
|
|
$this->assertTrue($signature['ltv']['embed_ocsp']);
|
||
|
|
$this->assertTrue($signature['ltv']['embed_crl']);
|
||
|
|
$this->assertTrue($signature['ltv']['embed_certs']);
|
||
|
|
$this->assertTrue($signature['ltv']['include_dss']);
|
||
|
|
$this->assertTrue($signature['ltv']['include_vri']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureThrowsWhenLtvIsNotArray(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$data = [
|
||
|
|
'appearance' => ['empty' => [], 'name' => '', 'page' => 0, 'rect' => ''],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 0,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => 'dummy-signcert',
|
||
|
|
'ltv' => 'invalid',
|
||
|
|
];
|
||
|
|
|
||
|
|
$setSignatureObj = new \ReflectionMethod($obj, 'setSignature');
|
||
|
|
$setSignatureObj->invoke($obj, $data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignatureThrowsWhenLtvKeyIsInvalidType(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$data = [
|
||
|
|
'appearance' => ['empty' => [], 'name' => '', 'page' => 0, 'rect' => ''],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 0,
|
||
|
|
'extracerts' => null,
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => 'dummy-signcert',
|
||
|
|
'ltv' => [
|
||
|
|
'enabled' => true,
|
||
|
|
'embed_ocsp' => true,
|
||
|
|
'embed_crl' => true,
|
||
|
|
'embed_certs' => true,
|
||
|
|
'include_dss' => true,
|
||
|
|
'include_vri' => 'yes',
|
||
|
|
],
|
||
|
|
];
|
||
|
|
|
||
|
|
$setSignatureObj = new \ReflectionMethod($obj, 'setSignature');
|
||
|
|
$setSignatureObj->invoke($obj, $data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignTimeStampThrowsOnInvalidPolicyOid(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$obj->setSignTimeStamp([
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => 'https://tsa.example.test',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha256',
|
||
|
|
'policy_oid' => 'invalid-oid',
|
||
|
|
'nonce_enabled' => true,
|
||
|
|
'timeout' => 5,
|
||
|
|
'verify_peer' => true,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignTimeStampThrowsOnInvalidNonceType(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$data = [
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => 'https://tsa.example.test',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha256',
|
||
|
|
'policy_oid' => '',
|
||
|
|
'nonce_enabled' => 1,
|
||
|
|
'timeout' => 5,
|
||
|
|
'verify_peer' => true,
|
||
|
|
];
|
||
|
|
|
||
|
|
$setSignTimeStampObj = new \ReflectionMethod($obj, 'setSignTimeStamp');
|
||
|
|
$setSignTimeStampObj->invoke($obj, $data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignTimeStampThrowsOnInvalidTimeout(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$obj->setSignTimeStamp([
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => 'https://tsa.example.test',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha256',
|
||
|
|
'policy_oid' => '',
|
||
|
|
'nonce_enabled' => true,
|
||
|
|
'timeout' => 0,
|
||
|
|
'verify_peer' => true,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSetSignTimeStampThrowsOnInvalidVerifyPeerType(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
|
||
|
|
|
||
|
|
$data = [
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => 'https://tsa.example.test',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha256',
|
||
|
|
'policy_oid' => '',
|
||
|
|
'nonce_enabled' => true,
|
||
|
|
'timeout' => 5,
|
||
|
|
'verify_peer' => 1,
|
||
|
|
];
|
||
|
|
|
||
|
|
$setSignTimeStampObj = new \ReflectionMethod($obj, 'setSignTimeStamp');
|
||
|
|
$setSignTimeStampObj->invoke($obj, $data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testAddPagePartialMarginsKeepBottomFooterMarginsWhenCtCbMissing(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
|
||
|
|
$margin = [
|
||
|
|
'PT' => 7,
|
||
|
|
'PR' => 7,
|
||
|
|
'PB' => 7,
|
||
|
|
'PL' => 7,
|
||
|
|
'HB' => 15,
|
||
|
|
'FT' => 15,
|
||
|
|
];
|
||
|
|
|
||
|
|
$landscape = $obj->addPage([
|
||
|
|
'orientation' => 'L',
|
||
|
|
'format' => 'A4',
|
||
|
|
'margin' => $margin,
|
||
|
|
]);
|
||
|
|
/** @var array{margin: array{CB: float, CT: float, FT: float, HB: float, PB: float}, FooterHeight: float, region: array<int, array{RB: float}>} $landscape */
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $landscape['margin']['CB']);
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $landscape['margin']['CT']);
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $landscape['margin']['FT']);
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $landscape['margin']['HB']);
|
||
|
|
$this->bcAssertEqualsWithDelta(7.0, $landscape['margin']['PB']);
|
||
|
|
$this->bcAssertEqualsWithDelta(8.0, $landscape['FooterHeight']);
|
||
|
|
$landscapeRegion0 = $landscape['region'][0] ?? null;
|
||
|
|
if (!\is_array($landscapeRegion0)) {
|
||
|
|
$this->fail('Unexpected addPage() region payload for landscape page.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $landscapeRegion0['RB']);
|
||
|
|
|
||
|
|
$portrait = $obj->addPage([
|
||
|
|
'orientation' => 'P',
|
||
|
|
'format' => 'A4',
|
||
|
|
'margin' => $margin,
|
||
|
|
]);
|
||
|
|
/** @var array{margin: array{CB: float, CT: float, FT: float, HB: float, PB: float}, FooterHeight: float, region: array<int, array{RB: float}>} $portrait */
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $portrait['margin']['CB']);
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $portrait['margin']['CT']);
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $portrait['margin']['FT']);
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $portrait['margin']['HB']);
|
||
|
|
$this->bcAssertEqualsWithDelta(7.0, $portrait['margin']['PB']);
|
||
|
|
$this->bcAssertEqualsWithDelta(8.0, $portrait['FooterHeight']);
|
||
|
|
$portraitRegion0 = $portrait['region'][0] ?? null;
|
||
|
|
if (!\is_array($portraitRegion0)) {
|
||
|
|
$this->fail('Unexpected addPage() region payload for portrait page.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->bcAssertEqualsWithDelta(15.0, $portraitRegion0['RB']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testPdfColorGetterAndInvalidColorFallback(): void
|
||
|
|
{
|
||
|
|
$default = $this->getTestObject();
|
||
|
|
/** @var \Com\Tecnick\Pdf\PdfColor $defaultColor */
|
||
|
|
$defaultColor = $this->getObjectProperty($default, 'color');
|
||
|
|
$this->assertFalse($defaultColor->isForceDeviceCmyk());
|
||
|
|
|
||
|
|
$pdfx = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, true, 'pdfx');
|
||
|
|
/** @var \Com\Tecnick\Pdf\PdfColor $pdfxColor */
|
||
|
|
$pdfxColor = $this->getObjectProperty($pdfx, 'color');
|
||
|
|
$this->assertTrue($pdfxColor->isForceDeviceCmyk());
|
||
|
|
$this->assertSame('', $pdfxColor->getPdfColor('not-a-color-value'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* End-to-end: signing a document embeds a native detached CAdES CMS that
|
||
|
|
* cryptographically verifies over the ByteRange bytes, while the legacy
|
||
|
|
* profile keeps the /adbe.pkcs7.detached SubFilter.
|
||
|
|
*
|
||
|
|
* @throws \Throwable
|
||
|
|
*/
|
||
|
|
public function testSignDocumentEmbedsVerifiableNativeCms(): void
|
||
|
|
{
|
||
|
|
$cred = $this->makeSigningCredentials();
|
||
|
|
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->setSignature([
|
||
|
|
'appearance' => ['empty' => [], 'name' => 'MainSig', 'page' => $page['pid'], 'rect' => '0 0 10 5'],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => '',
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => $cred['key_pem'],
|
||
|
|
'signcert' => $cred['cert_pem'],
|
||
|
|
]);
|
||
|
|
|
||
|
|
$pdf = $obj->getOutPDFString();
|
||
|
|
|
||
|
|
// Legacy profile keeps the ISO 32000-1 SubFilter.
|
||
|
|
$this->assertStringContainsString('/SubFilter /adbe.pkcs7.detached', $pdf);
|
||
|
|
|
||
|
|
[$cmsDer, $signedBytes] = $this->extractSignatureCmsAndContent($pdf);
|
||
|
|
|
||
|
|
// The embedded blob is a CMS SignedData ContentInfo.
|
||
|
|
$asn1 = new \Com\Tecnick\Pdf\Sign\Cms\Asn1();
|
||
|
|
$offset = 0;
|
||
|
|
$root = $asn1->readTlv($cmsDer, $offset);
|
||
|
|
$this->assertSame(0x30, $root['tag']);
|
||
|
|
$this->assertStringContainsString($asn1->encodeObjectIdentifier('1.2.840.113549.1.7.2'), $cmsDer);
|
||
|
|
|
||
|
|
// The detached signature covers exactly the ByteRange bytes: the
|
||
|
|
// message-digest signed attribute equals SHA-256 of that content.
|
||
|
|
$this->assertStringContainsString(\hash('sha256', $signedBytes, true), $cmsDer);
|
||
|
|
|
||
|
|
// The SignerInfo signature verifies over the DER SET OF signed attributes.
|
||
|
|
$this->assertTrue($this->cmsSignatureVerifies($cmsDer, $cred['cert_pem']));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* PAdES B-LT: the Document Security Store is emitted in a post-signing
|
||
|
|
* incremental revision whose VRI key is the SHA-1 of the final signature
|
||
|
|
* /Contents, and the document catalog is re-emitted there with the /DSS
|
||
|
|
* reference. The originally signed revision is left byte-for-byte intact.
|
||
|
|
*
|
||
|
|
* @throws \Throwable
|
||
|
|
*/
|
||
|
|
public function testGetOutPDFStringAppendsBltDssRevision(): void
|
||
|
|
{
|
||
|
|
$cred = $this->makeSigningCredentials();
|
||
|
|
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
$obj->setSignature([
|
||
|
|
'appearance' => ['empty' => [], 'name' => 'MainSig', 'page' => $page['pid'], 'rect' => '0 0 10 5'],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => '',
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => $cred['key_pem'],
|
||
|
|
'signcert' => $cred['cert_pem'],
|
||
|
|
'ltv' => [
|
||
|
|
'enabled' => true,
|
||
|
|
'embed_ocsp' => false,
|
||
|
|
'embed_crl' => false,
|
||
|
|
'embed_certs' => true,
|
||
|
|
'include_dss' => true,
|
||
|
|
'include_vri' => true,
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
|
||
|
|
$pdf = $obj->getOutPDFString();
|
||
|
|
|
||
|
|
// The signed revision is untouched and a second (DSS) revision is appended.
|
||
|
|
$this->assertSame(2, \substr_count($pdf, '%%EOF'));
|
||
|
|
$this->assertSame(2, \substr_count($pdf, "startxref\n"));
|
||
|
|
|
||
|
|
$firstEof = \strpos($pdf, '%%EOF');
|
||
|
|
$this->assertIsInt($firstEof);
|
||
|
|
$revision = \substr($pdf, $firstEof);
|
||
|
|
|
||
|
|
// The DSS objects and the re-emitted catalog live only in the appended revision.
|
||
|
|
$this->assertStringContainsString('/Type /DSS', $revision);
|
||
|
|
$this->assertStringContainsString('/Certs [', $revision);
|
||
|
|
$this->assertStringContainsString('/Type /VRI', $revision);
|
||
|
|
$this->assertStringContainsString('/Type /Catalog', $revision);
|
||
|
|
$this->assertStringContainsString('/Prev ', $revision);
|
||
|
|
|
||
|
|
// The VRI key is the uppercase SHA-1 of the signature /Contents bytes.
|
||
|
|
$cstart = \strpos($pdf, '/Contents<');
|
||
|
|
$this->assertIsInt($cstart);
|
||
|
|
$cstart += \strlen('/Contents<');
|
||
|
|
$cend = \strpos($pdf, '>', $cstart);
|
||
|
|
$this->assertIsInt($cend);
|
||
|
|
$contents = (string) \hex2bin(\substr($pdf, $cstart, $cend - $cstart));
|
||
|
|
$vriKey = \strtoupper(\sha1($contents));
|
||
|
|
$this->assertStringContainsString('/VRI << /' . $vriKey . ' ', $revision);
|
||
|
|
$this->assertStringContainsString('/DSS ', $revision);
|
||
|
|
|
||
|
|
// It is no longer the legacy cert-hash VRI key.
|
||
|
|
$certDer = (string) \base64_decode(
|
||
|
|
(string) \preg_replace('/-----[^-]+-----|\s+/', '', $cred['cert_pem']),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
$this->assertStringNotContainsString(\strtoupper(\sha1($certDer)), $revision);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSignatureFacadeIsLazyAndStable(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$facade = $obj->signature();
|
||
|
|
|
||
|
|
$this->assertInstanceOf(\Com\Tecnick\Pdf\Signature\Facade::class, $facade);
|
||
|
|
$this->assertSame($facade, $obj->signature());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSignatureFacadeConfigureSetsSignState(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$result = $obj->signature()->configure([
|
||
|
|
'appearance' => ['empty' => [], 'name' => 'MainSig', 'page' => $page['pid'], 'rect' => '0 0 10 5'],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => '',
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => 'dummy-key',
|
||
|
|
'signcert' => 'dummy-cert',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$this->assertInstanceOf(\Com\Tecnick\Pdf\Signature\Facade::class, $result);
|
||
|
|
$this->assertTrue($this->getObjectProperty($obj, 'sign'));
|
||
|
|
$this->assertGreaterThan(0, $obj->signature()->widgetObjectId());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSignatureFacadeAppearanceAndEmptyFieldDelegate(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$appearance = $obj->signature()->appearance();
|
||
|
|
$chained = $appearance->place(10, 20, 30, 15, $page['pid'], 'MainSig')->xobject('IMP1');
|
||
|
|
$appearance->stream('q 0 g Q', 'N', 'On');
|
||
|
|
$obj->signature()->emptyField(5, 6, 20, 10, $page['pid'], 'EmptySig');
|
||
|
|
|
||
|
|
$this->assertInstanceOf(\Com\Tecnick\Pdf\Signature\Appearance::class, $chained);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var array{appearance: array{
|
||
|
|
* name: string, xobj: string, as: string, empty: array<int, array{name: string}>
|
||
|
|
* }} $signature
|
||
|
|
*/
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertSame('MainSig', $signature['appearance']['name']);
|
||
|
|
$this->assertSame('IMP1', $signature['appearance']['xobj']);
|
||
|
|
$this->assertSame('On', $signature['appearance']['as']);
|
||
|
|
$this->assertNotEmpty($signature['appearance']['empty']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSignatureFacadeTimestampAndUserRightsDelegate(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
|
||
|
|
$obj
|
||
|
|
->signature()
|
||
|
|
->timestamp([
|
||
|
|
'enabled' => true,
|
||
|
|
'host' => 'https://tsa.example.org',
|
||
|
|
'username' => '',
|
||
|
|
'password' => '',
|
||
|
|
'cert' => '',
|
||
|
|
'hash_algorithm' => 'sha256',
|
||
|
|
'policy_oid' => '',
|
||
|
|
'nonce_enabled' => true,
|
||
|
|
'timeout' => 5,
|
||
|
|
'verify_peer' => true,
|
||
|
|
])
|
||
|
|
->userRights([
|
||
|
|
'annots' => '',
|
||
|
|
'document' => '/FullSave',
|
||
|
|
'ef' => '',
|
||
|
|
'enabled' => true,
|
||
|
|
'form' => '',
|
||
|
|
'formex' => '',
|
||
|
|
'signature' => '',
|
||
|
|
]);
|
||
|
|
|
||
|
|
/** @var array{enabled: bool, host: string} $ts */
|
||
|
|
$ts = $this->getObjectProperty($obj, 'sigtimestamp');
|
||
|
|
$this->assertTrue($ts['enabled']);
|
||
|
|
$this->assertSame('https://tsa.example.org', $ts['host']);
|
||
|
|
|
||
|
|
/** @var array{document: string} $rights */
|
||
|
|
$rights = $this->getObjectProperty($obj, 'userrights');
|
||
|
|
$this->assertSame('/FullSave', $rights['document']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSignatureFacadeUpgradeToLtaSetsProfileAndDss(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
|
||
|
|
$result = $obj->signature()->upgradeToLta();
|
||
|
|
|
||
|
|
$this->assertInstanceOf(\Com\Tecnick\Pdf\Signature\Facade::class, $result);
|
||
|
|
|
||
|
|
/** @var array{profile: string, ltv: array{enabled: bool, include_dss: bool}} $signature */
|
||
|
|
$signature = $this->getObjectProperty($obj, 'signature');
|
||
|
|
$this->assertSame('pades-b-lta', $signature['profile']);
|
||
|
|
$this->assertTrue($signature['ltv']['enabled']);
|
||
|
|
$this->assertTrue($signature['ltv']['include_dss']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @throws \Throwable */
|
||
|
|
public function testSignatureFacadeExternalConfigureAndPrepare(): void
|
||
|
|
{
|
||
|
|
$obj = $this->getTestObject();
|
||
|
|
$page = $this->initFontAndAddRawPage($obj);
|
||
|
|
|
||
|
|
$obj
|
||
|
|
->signature()
|
||
|
|
->external()
|
||
|
|
->configure([
|
||
|
|
'appearance' => ['empty' => [], 'name' => 'ExtSig', 'page' => $page['pid'], 'rect' => '0 0 10 5'],
|
||
|
|
'approval' => '',
|
||
|
|
'cert_type' => 2,
|
||
|
|
'extracerts' => '',
|
||
|
|
'info' => ['ContactInfo' => '', 'Location' => '', 'Name' => '', 'Reason' => ''],
|
||
|
|
'password' => '',
|
||
|
|
'privkey' => '',
|
||
|
|
'signcert' => '',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$prep = $obj->signature()->external()->prepare('sha256');
|
||
|
|
$this->assertSame('sha256', $prep['algorithm']);
|
||
|
|
$this->assertNotSame('', $prep['prepared_pdf']);
|
||
|
|
$this->assertSame(\hash('sha256', $prep['prepared_pdf'], true), $prep['hash_raw']);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Extract the embedded CMS (DER, trimmed of placeholder padding) and the
|
||
|
|
* ByteRange-covered content from a signed PDF.
|
||
|
|
*
|
||
|
|
* @return array{string, string} [CMS DER, signed content bytes]
|
||
|
|
* @throws \Throwable
|
||
|
|
*/
|
||
|
|
private function extractSignatureCmsAndContent(string $pdf): array
|
||
|
|
{
|
||
|
|
$m = [];
|
||
|
|
if (\preg_match('/\\/ByteRange\\[0 (\\d+) (\\d+) (\\d+)\\]/', $pdf, $m) !== 1) {
|
||
|
|
$this->fail('ByteRange not found in signed output');
|
||
|
|
}
|
||
|
|
|
||
|
|
$end1 = (int) ($m[1] ?? '0');
|
||
|
|
$start2 = (int) ($m[2] ?? '0');
|
||
|
|
$len2 = (int) ($m[3] ?? '0');
|
||
|
|
|
||
|
|
$signedBytes = \substr($pdf, 0, $end1) . \substr($pdf, $start2, $len2);
|
||
|
|
|
||
|
|
$contents = \substr($pdf, $end1, $start2 - $end1); // '<' hex '>'
|
||
|
|
$hex = \trim($contents, '<>');
|
||
|
|
$binary = (string) \hex2bin($hex);
|
||
|
|
|
||
|
|
// Trim the trailing zero padding to the exact DER length.
|
||
|
|
$asn1 = new \Com\Tecnick\Pdf\Sign\Cms\Asn1();
|
||
|
|
$offset = 0;
|
||
|
|
$cms = $asn1->readTlv($binary, $offset);
|
||
|
|
|
||
|
|
return [$cms['raw'], $signedBytes];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cryptographically verify the CMS SignerInfo signature over its signed
|
||
|
|
* attributes using the certificate public key.
|
||
|
|
*
|
||
|
|
* @throws \Throwable
|
||
|
|
*/
|
||
|
|
private function cmsSignatureVerifies(string $cmsDer, string $certPem): bool
|
||
|
|
{
|
||
|
|
$asn1 = new \Com\Tecnick\Pdf\Sign\Cms\Asn1();
|
||
|
|
|
||
|
|
$offset = 0;
|
||
|
|
$contentInfo = $asn1->readTlv($cmsDer, $offset);
|
||
|
|
$ci = 0;
|
||
|
|
$asn1->readTlv($contentInfo['value'], $ci); // contentType OID
|
||
|
|
$explicit = $asn1->readTlv($contentInfo['value'], $ci); // [0] EXPLICIT
|
||
|
|
|
||
|
|
$sd = 0;
|
||
|
|
$signedData = $asn1->readTlv($explicit['value'], $sd);
|
||
|
|
$inner = 0;
|
||
|
|
$asn1->readTlv($signedData['value'], $inner); // version
|
||
|
|
$asn1->readTlv($signedData['value'], $inner); // digestAlgorithms
|
||
|
|
$asn1->readTlv($signedData['value'], $inner); // encapContentInfo
|
||
|
|
$asn1->readTlv($signedData['value'], $inner); // certificates [0]
|
||
|
|
$signerInfos = $asn1->readTlv($signedData['value'], $inner); // signerInfos SET
|
||
|
|
|
||
|
|
$si = 0;
|
||
|
|
$signerInfo = $asn1->readTlv($signerInfos['value'], $si);
|
||
|
|
$f = 0;
|
||
|
|
$asn1->readTlv($signerInfo['value'], $f); // version
|
||
|
|
$asn1->readTlv($signerInfo['value'], $f); // sid
|
||
|
|
$asn1->readTlv($signerInfo['value'], $f); // digestAlgorithm
|
||
|
|
$signedAttrs = $asn1->readTlv($signerInfo['value'], $f); // [0] IMPLICIT
|
||
|
|
$asn1->readTlv($signerInfo['value'], $f); // signatureAlgorithm
|
||
|
|
$signature = $asn1->readTlv($signerInfo['value'], $f); // signature OCTET STRING
|
||
|
|
|
||
|
|
$publicKey = \openssl_pkey_get_public($certPem);
|
||
|
|
if ($publicKey === false) {
|
||
|
|
$this->fail('Unable to load certificate public key');
|
||
|
|
}
|
||
|
|
|
||
|
|
$signedAttrsSet = $asn1->encodeSet($signedAttrs['value']);
|
||
|
|
return \openssl_verify($signedAttrsSet, $signature['value'], $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate an RSA signing key and a matching self-signed certificate (PEM).
|
||
|
|
*
|
||
|
|
* @return array{cert_pem: string, key_pem: string}
|
||
|
|
*/
|
||
|
|
private function makeSigningCredentials(): array
|
||
|
|
{
|
||
|
|
$config = ['digest_alg' => 'sha256', 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA];
|
||
|
|
$key = \openssl_pkey_new($config);
|
||
|
|
if (!$key instanceof \OpenSSLAsymmetricKey) {
|
||
|
|
$this->markTestSkipped('RSA key generation is not available');
|
||
|
|
}
|
||
|
|
|
||
|
|
$csr = \openssl_csr_new(['commonName' => 'tc-lib-pdf signer'], $key, $config);
|
||
|
|
if (!$csr instanceof \OpenSSLCertificateSigningRequest) {
|
||
|
|
$this->markTestSkipped('CSR generation failed');
|
||
|
|
}
|
||
|
|
|
||
|
|
$cert = \openssl_csr_sign($csr, null, $key, 365, $config);
|
||
|
|
if (!$cert instanceof \OpenSSLCertificate) {
|
||
|
|
$this->markTestSkipped('Certificate signing failed');
|
||
|
|
}
|
||
|
|
|
||
|
|
$certPem = '';
|
||
|
|
\openssl_x509_export($cert, $certPem);
|
||
|
|
$keyPem = '';
|
||
|
|
\openssl_pkey_export($key, $keyPem);
|
||
|
|
|
||
|
|
return ['cert_pem' => $certPem, 'key_pem' => $keyPem];
|
||
|
|
}
|
||
|
|
}
|