From cda07d5aa44c653d398345ef48e5d398a52c1c4b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:49:35 -0300 Subject: [PATCH 01/13] feat(test): create PdfGenerator class with static methods Consolidate PDF fixture generation into a single static class: - Migrate all methods from PdfFixtureTrait to PdfGenerator - Convert trait methods to public static methods - Maintain all existing functionality - Includes 20+ methods for ISO 32000-1 edge case testing This improves code organization and eliminates trait usage. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/fixtures/PdfGenerator.php | 636 ++++++++++++++++++++++++++++ 1 file changed, 636 insertions(+) create mode 100644 tests/php/fixtures/PdfGenerator.php diff --git a/tests/php/fixtures/PdfGenerator.php b/tests/php/fixtures/PdfGenerator.php new file mode 100644 index 0000000000..afc856d5a4 --- /dev/null +++ b/tests/php/fixtures/PdfGenerator.php @@ -0,0 +1,636 @@ +>\nendobj\n" + . "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" + . "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" + . "xref\n0 4\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n190\n%%EOF"; + } + + public static function createPdfWithDocMdp(int $pValue, bool $withModifications = false): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + if ($withModifications) { + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; + $pdf .= "xref\n7 1\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + } else { + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + } + + return $pdf; + } + + public static function createPdfWithFormFieldModification(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [9 0 R] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 9 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n9 0 obj\n<< /FT /Tx /T (TextField1) /V (Modified) /Rect [100 100 200 120] >>\nendobj\n"; + $pdf .= "xref\n0 10\ntrailer\n<< /Size 10 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + public static function createPdfWithAnnotationModification(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; + $pdf .= "xref\n7 1\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + public static function createResourceFromContent(string $content) { + $resource = tmpfile(); + fwrite($resource, $content); + rewind($resource); + return $resource; + } + + /** + * FPDI-compliant PDF structure (for FileService validation) + * Creates a complete PDF with proper xref table and stream objects + */ + public static function createCompletePdfStructure(int $pValue, bool $withModifications = false): string { + $pdf = "%PDF-1.7\n"; + + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 7 0 R /Resources << /Font << /F1 8 0 R >> >> >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $currentLength = strlen($pdf); + $signatureObjectStart = $currentLength + 150; + $signatureLength = 8192; + $offset1 = 0; + $length1 = $signatureObjectStart; + $offset2 = $signatureObjectStart + $signatureLength; + + $sigObj = "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $sigObj .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $sigObj .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $sigObj .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $sigObj .= '/Contents <' . str_repeat('30', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= $sigObj; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R /Rect [0 0 0 0] /P 3 0 R >>\nendobj\n"; + $pdf .= "7 0 obj\n<< /Length 44 >>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Test Document) Tj\nET\nendstream\nendobj\n"; + $pdf .= "8 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"; + + $length2 = 300; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + if ($withModifications) { + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + $pdf .= "\n9 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; + } + + $xrefPos = strlen($pdf); + $objectCount = $withModifications ? 10 : 9; + $pdf .= "xref\n0 $objectCount\n"; + $pdf .= "0000000000 65535 f \n"; + $pdf .= "0000000015 00000 n \n"; + $pdf .= "0000000115 00000 n \n"; + $pdf .= "0000000174 00000 n \n"; + $pdf .= "0000000308 00000 n \n"; + $pdf .= sprintf("%010d 00000 n \n", $currentLength); + $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj)); + $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj) + 100); + $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj) + 200); + if ($withModifications) { + $pdf .= sprintf("%010d 00000 n \n", $xrefPos - 100); + } + + $pdf .= "trailer\n<< /Size $objectCount /Root 1 0 R >>\n"; + $pdf .= "startxref\n$xrefPos\n%%EOF\n"; + + return $pdf; + } + + /** + * DocMDP signature without /V entry (ISO 32000-1 violation) + */ + public static function createPdfWithDocMdpWithoutVersion(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * DocMDP signature with non-standard version + */ + public static function createPdfWithDocMdpInvalidVersion(int $pValue, string $version): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /$version >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * PDF with page added after signing (tests P=1 rejection) + */ + public static function createPdfWithStructuralModification(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R 7 0 R] /Count 2 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + /** + * PDF with second signature added after DocMDP (tests ISO 32000-2 §12.8.2.3) + */ + public static function createPdfWithSubsequentSignature(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; + $pdf .= "8 0 obj\n<< /FT /Sig /T (Signature2) /V 7 0 R >>\nendobj\n"; + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + /** + * DocMDP in /Reference but not in /Perms (tests §12.8.2.2 validation) + */ + public static function createPdfWithDocMdpInSignatureReference(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Approval signature, then certification (violates "DocMDP must be first") + */ + public static function createPdfWithApprovalThenCertifyingSignature(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P 1 /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "8 0 obj\n<< /FT /Sig /T (CertifyingSignature) /V 7 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * PDF with XObject form added after signing (tests P=3 acceptance) + */ + public static function createPdfWithPageTemplate(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] >>\nendobj\n"; + $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + /** + * ITI's indirect reference style: /Reference [ 7 0 R ] instead of inline dict + */ + public static function createPdfWithIndirectReferencesItiStyle(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 300; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ 7 0 R ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; + + $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /1.2 >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Indirect references with invalid /V value + */ + public static function createPdfWithIndirectReferencesInvalidVersion(int $pValue, string $version): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 350; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/M (D:20220705145549-03'00')\n"; + $pdf .= "/Reference [7 0 R]\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; + $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /$version >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $startxref = strlen($pdf); + $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; + + return $pdf; + } + + /** + * Signature with wrong /Type (not /Sig) + */ + public static function createPdfWithInvalidSignatureType(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /InvalidType /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * Signature without /Filter entry + */ + public static function createPdfWithoutFilterEntry(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /Sig /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * Signature without /ByteRange + */ + public static function createPdfWithoutByteRange(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * Two signatures both with DocMDP (forbidden per ISO 32000-2 §12.8.2.2) + */ + public static function createPdfWithMultipleDocMdpSignatures(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 200 100]\n"; + $pdf .= "/Reference [7 0 R] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; + + $pdf .= "8 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 300 100]\n"; + $pdf .= "/Reference [9 0 R] >>\nendobj\n"; + $pdf .= "9 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 3 /V /1.2 >> >>\nendobj\n"; + $pdf .= "10 0 obj\n<< /FT /Sig /T (Signature2) /V 8 0 R >>\nendobj\n"; + + $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * DocMDP not on first signature (violates first-signature-only rule) + */ + public static function createPdfWithDocMdpNotFirst(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 200 100] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; + + $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 300 100]\n"; + $pdf .= "/Reference [8 0 R] >>\nendobj\n"; + $pdf .= "8 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; + $pdf .= "10 0 obj\n<< /FT /Sig /T (CertificationSignature) /V 7 0 R >>\nendobj\n"; + + $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } + + /** + * SigRef without /TransformMethod (ISO 32000-1 violation) + */ + public static function createPdfWithSigRefWithoutTransformMethod(): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [0 100 200 100]\n"; + $pdf .= "/Reference [<< /Type /SigRef /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; + return $pdf; + } +} From 4f8546f3180b4022589c20e1510a0a2d4f82ba1e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:52:05 -0300 Subject: [PATCH 02/13] refactor(test): remove PdfFixtureTrait Delete trait after migrating all methods to PdfGenerator. The trait pattern was unnecessary for stateless utility methods. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/PdfFixtureTrait.php | 681 ----------------------------- 1 file changed, 681 deletions(-) delete mode 100644 tests/php/Unit/PdfFixtureTrait.php diff --git a/tests/php/Unit/PdfFixtureTrait.php b/tests/php/Unit/PdfFixtureTrait.php deleted file mode 100644 index 737a943732..0000000000 --- a/tests/php/Unit/PdfFixtureTrait.php +++ /dev/null @@ -1,681 +0,0 @@ ->\nendobj\n" - . "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" - . "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" - . "xref\n0 4\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n190\n%%EOF"; - } - - /** - * Create a complete PDF with DocMDP signature - * - * This creates a more complete PDF structure that passes FPDI validation - * and includes proper DocMDP transformation parameters. - * - * @param int $pValue DocMDP permission level (0=not certified, 1=no changes, 2=form filling, 3=form+annotations) - * @param bool $withModifications Whether to add modifications after signature - * @return string PDF content as string - */ - /** - * Create PDF with DocMDP signature - * - * Uses complete FPDI-valid structure for FileService tests, - * or minimal structure for DocMdpHandler tests. - */ - public function createPdfWithDocMdp(int $pValue, bool $withModifications = false): string { - // FileService needs FPDI-valid PDF (has validatePdfStringWithFpdi) - if (str_contains(static::class, 'FileServiceTest')) { - return $this->createCompletePdfStructure($pValue, $withModifications); - } - - // DocMdpHandler only needs minimal structure - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - if ($withModifications) { - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; - $pdf .= "xref\n7 1\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - } else { - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - } - - return $pdf; - } - - /** - * FPDI-compliant PDF structure (for FileService validation) - * - * FileService.validateFileContent() uses Smalot PDF parser which requires: - * - Valid xref table with correct offsets - * - Content streams - * - Font dictionaries - * - Proper trailer - */ - private function createCompletePdfStructure(int $pValue, bool $withModifications): string { - $pdf = "%PDF-1.7\n"; - - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 7 0 R /Resources << /Font << /F1 8 0 R >> >> >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $currentLength = strlen($pdf); - $signatureObjectStart = $currentLength + 150; - $signatureLength = 8192; - $offset1 = 0; - $length1 = $signatureObjectStart; - $offset2 = $signatureObjectStart + $signatureLength; - - $sigObj = "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $sigObj .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $sigObj .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $sigObj .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $sigObj .= '/Contents <' . str_repeat('30', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= $sigObj; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R /Rect [0 0 0 0] /P 3 0 R >>\nendobj\n"; - $pdf .= "7 0 obj\n<< /Length 44 >>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Test Document) Tj\nET\nendstream\nendobj\n"; - $pdf .= "8 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"; - - $length2 = 300; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - if ($withModifications) { - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - $pdf .= "\n9 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; - } - - $xrefPos = strlen($pdf); - $objectCount = $withModifications ? 10 : 9; - $pdf .= "xref\n0 $objectCount\n"; - $pdf .= "0000000000 65535 f \n"; - $pdf .= "0000000015 00000 n \n"; - $pdf .= "0000000115 00000 n \n"; - $pdf .= "0000000174 00000 n \n"; - $pdf .= "0000000308 00000 n \n"; - $pdf .= sprintf("%010d 00000 n \n", $currentLength); - $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj)); - $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj) + 100); - $pdf .= sprintf("%010d 00000 n \n", $currentLength + strlen($sigObj) + 200); - if ($withModifications) { - $pdf .= sprintf("%010d 00000 n \n", $xrefPos - 100); - } - - $pdf .= "trailer\n<< /Size $objectCount /Root 1 0 R >>\n"; - $pdf .= "startxref\n$xrefPos\n%%EOF\n"; - - return $pdf; - } - - protected function createPdfWithFormFieldModification(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 7 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /FT /Tx /T (TextField1) /V (Modified Value) >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with annotation modification - */ - protected function createPdfWithAnnotationModification(int $pValue): string { - return $this->createPdfWithDocMdp($pValue, withModifications: true); - } - - /** - * Create PDF with DocMDP but without version in TransformParams - */ - protected function createPdfWithDocMdpWithoutVersion(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - // Missing /V in TransformParams - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with DocMDP with invalid version - */ - protected function createPdfWithDocMdpInvalidVersion(int $pValue, string $version): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /$version >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with DocMDP version 1.2 (valid per ICP-Brasil) - */ - protected function createPdfWithDocMdpVersion12(int $pValue): string { - return $this->createPdfWithDocMdp($pValue); - } - - /** - * Convenience methods for specific DocMDP levels - */ - protected function createPdfWithDocMdpLevel0(): string { - return $this->createPdfWithDocMdp(0); - } - - protected function createPdfWithDocMdpLevel1(): string { - return $this->createPdfWithDocMdp(1); - } - - protected function createPdfWithDocMdpLevel2(): string { - return $this->createPdfWithDocMdp(2); - } - - protected function createPdfWithDocMdpLevel3(): string { - return $this->createPdfWithDocMdp(3); - } - - /** - * Create resource from PDF content (for DocMdpHandler tests) - */ - protected function createResourceFromContent(string $content) { - $resource = tmpfile(); - fwrite($resource, $content); - fseek($resource, 0); - return $resource; - } - - /** - * Create PDF with structural modification (adding a new page) - */ - protected function createPdfWithStructuralModification(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R 7 0 R] /Count 2 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with subsequent signature (multiple signatures) - */ - protected function createPdfWithSubsequentSignature(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; - $pdf .= "8 0 obj\n<< /FT /Sig /T (Signature2) /V 7 0 R >>\nendobj\n"; - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with DocMDP in signature Reference (without /Perms) - */ - protected function createPdfWithDocMdpInSignatureReference(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with approval signature followed by certifying signature - */ - protected function createPdfWithApprovalThenCertifyingSignature(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 8 0 R] >>\nendobj\n"; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= '/ByteRange [ 0 100 200 100 ] /Contents <' . str_repeat('00', 50) . "> >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P 1 /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "8 0 obj\n<< /FT /Sig /T (CertifyingSignature) /V 7 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with page template (XObject Form) - */ - protected function createPdfWithPageTemplate(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with indirect references (ITI style) - */ - protected function createPdfWithIndirectReferencesItiStyle(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 300; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ 7 0 R ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; - - $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /1.2 >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - /** - * Create PDF with indirect references and invalid version (for testing rejection) - */ - protected function createPdfWithIndirectReferencesInvalidVersion(int $pValue, string $version): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 350; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/M (D:20220705145549-03'00')\n"; - $pdf .= "/Reference [7 0 R]\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams 8 0 R >>\nendobj\n"; - $pdf .= "8 0 obj\n<< /Type /TransformParams /P $pValue /V /$version >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $startxref = strlen($pdf); - $pdf .= "xref\n0 9\ntrailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n$startxref\n%%EOF"; - - return $pdf; - } - - /** - * ISO 32000-1 Table 252 validation: Signature dictionary with invalid /Type - */ - protected function createPdfWithInvalidSignatureType(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /InvalidType /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; - return $pdf; - } - - /** - * ISO 32000-1 Table 252 validation: Signature dictionary without /Filter - */ - protected function createPdfWithoutFilterEntry(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /Sig /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; - return $pdf; - } - - /** - * ISO 32000-1: Signature without required /ByteRange - */ - protected function createPdfWithoutByteRange(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; - return $pdf; - } - - /** - * ISO 32000-1 12.8.2.2.1: Multiple DocMDP signatures (invalid) - */ - protected function createPdfWithMultipleDocMdpSignatures(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; - - // First DocMDP signature - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 200 100]\n"; - $pdf .= "/Reference [7 0 R] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; - - // Second DocMDP signature (INVALID per ISO) - $pdf .= "8 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 300 100]\n"; - $pdf .= "/Reference [9 0 R] >>\nendobj\n"; - $pdf .= "9 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 3 /V /1.2 >> >>\nendobj\n"; - $pdf .= "10 0 obj\n<< /FT /Sig /T (Signature2) /V 8 0 R >>\nendobj\n"; - - $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; - return $pdf; - } - - /** - * ISO 32000-1 12.8.2.2.1: DocMDP not as first signature (invalid) - */ - protected function createPdfWithDocMdpNotFirst(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; - - // First signature: regular approval signature (no DocMDP) - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 200 100] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; - - // Second signature: DocMDP certification (INVALID - must be first) - $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 300 100]\n"; - $pdf .= "/Reference [8 0 R] >>\nendobj\n"; - $pdf .= "8 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; - $pdf .= "10 0 obj\n<< /FT /Sig /T (CertificationSignature) /V 7 0 R >>\nendobj\n"; - - $pdf .= "xref\n0 11\ntrailer\n<< /Size 11 /Root 1 0 R >>\n%%EOF"; - return $pdf; - } - - /** - * ISO 32000-1 Table 253: SigRef without /TransformMethod - */ - protected function createPdfWithSigRefWithoutTransformMethod(): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [0 100 200 100]\n"; - $pdf .= "/Reference [<< /Type /SigRef /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>] >>\nendobj\n"; - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - $pdf .= "xref\n0 7\ntrailer\n<< /Size 7 /Root 1 0 R >>\n%%EOF"; - return $pdf; - } -} From 8ddd6394f56c0ba96f2261b18cb7526a776c1944 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:52:16 -0300 Subject: [PATCH 03/13] refactor(test): migrate Unit tests to use PdfGenerator Replace PdfFixtureTrait usage with PdfGenerator static methods: - Remove 'use PdfFixtureTrait' statements - Add 'use OCA\Libresign\Tests\Fixtures\PdfGenerator' imports - Change trait method calls to static PdfGenerator:: calls - Fix incorrect PdfGenerator::createMock() to $this->createMock() Affected test files: - DocMdpHandlerTest - FileServiceTest (3 createMock fixes) - SignFileServiceTest (20+ createMock fixes) - PdfParseServiceTest - PdfSignatureDetectionServiceTest - RequestSignatureServiceTest - ValidateHelperTest - AEnvironmentPageAwareControllerTest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../AEnvironmentPageAwareControllerTest.php | 2 +- tests/php/Unit/Handler/DocMdpHandlerTest.php | 121 +++++++++--------- tests/php/Unit/Helper/ValidateHelperTest.php | 10 +- tests/php/Unit/Service/FileServiceTest.php | 28 ++-- .../php/Unit/Service/PdfParseServiceTest.php | 8 +- .../PdfSignatureDetectionServiceTest.php | 25 ++-- .../Service/RequestSignatureServiceTest.php | 8 +- .../php/Unit/Service/SignFileServiceTest.php | 18 ++- 8 files changed, 112 insertions(+), 108 deletions(-) diff --git a/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php b/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php index 7130340b98..aa0e0ba5ef 100644 --- a/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php +++ b/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php @@ -71,7 +71,7 @@ public function testLoadFileUuidWhenFileNotFound(): void { $user = $this->createAccount('username', 'password'); $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Unit/Handler/DocMdpHandlerTest.php b/tests/php/Unit/Handler/DocMdpHandlerTest.php index 6fc69294ea..9c87f7f47f 100644 --- a/tests/php/Unit/Handler/DocMdpHandlerTest.php +++ b/tests/php/Unit/Handler/DocMdpHandlerTest.php @@ -12,14 +12,13 @@ use OCA\Libresign\Db\File; use OCA\Libresign\Enum\DocMdpLevel; use OCA\Libresign\Handler\DocMdpHandler; -use OCA\Libresign\Tests\Unit\PdfFixtureTrait; +use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCP\IL10N; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; final class DocMdpHandlerTest extends TestCase { - use PdfFixtureTrait; private IL10N&MockObject $l10n; private DocMdpHandler $handler; @@ -30,9 +29,9 @@ protected function setUp(): void { } public function testUnsignedPdfIsDetectedAsLevelNone(): void { - $pdfContent = $this->createMinimalPdf(); + $pdfContent = PdfGenerator::createMinimalPdf(); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -40,9 +39,9 @@ public function testUnsignedPdfIsDetectedAsLevelNone(): void { } public function testP0AllowsAnyModification(): void { - $pdfContent = $this->createPdfWithDocMdp(0, withModifications: true); + $pdfContent = PdfGenerator::createPdfWithDocMdp(0, withModifications: true); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -51,9 +50,9 @@ public function testP0AllowsAnyModification(): void { } public function testP1ProhibitsAnyModification(): void { - $pdfContent = $this->createPdfWithDocMdp(1, withModifications: true); + $pdfContent = PdfGenerator::createPdfWithDocMdp(1, withModifications: true); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -62,9 +61,9 @@ public function testP1ProhibitsAnyModification(): void { } public function testP2AllowsFormFieldModifications(): void { - $pdfContent = $this->createPdfWithFormFieldModification(2); + $pdfContent = PdfGenerator::createPdfWithFormFieldModification(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -73,9 +72,9 @@ public function testP2AllowsFormFieldModifications(): void { } public function testP2ProhibitsAnnotationModifications(): void { - $pdfContent = $this->createPdfWithAnnotationModification(2); + $pdfContent = PdfGenerator::createPdfWithAnnotationModification(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -84,9 +83,9 @@ public function testP2ProhibitsAnnotationModifications(): void { } public function testP3AllowsFormFieldModifications(): void { - $pdfContent = $this->createPdfWithFormFieldModification(3); + $pdfContent = PdfGenerator::createPdfWithFormFieldModification(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -95,9 +94,9 @@ public function testP3AllowsFormFieldModifications(): void { } public function testP3AllowsAnnotationModifications(): void { - $pdfContent = $this->createPdfWithAnnotationModification(3); + $pdfContent = PdfGenerator::createPdfWithAnnotationModification(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -106,9 +105,9 @@ public function testP3AllowsAnnotationModifications(): void { } public function testP3ProhibitsStructuralModifications(): void { - $pdfContent = $this->createPdfWithStructuralModification(3); + $pdfContent = PdfGenerator::createPdfWithStructuralModification(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -117,9 +116,9 @@ public function testP3ProhibitsStructuralModifications(): void { } public function testP2AllowsSubsequentSignatures(): void { - $pdfContent = $this->createPdfWithSubsequentSignature(2); + $pdfContent = PdfGenerator::createPdfWithSubsequentSignature(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -128,9 +127,9 @@ public function testP2AllowsSubsequentSignatures(): void { } public function testP3AllowsSubsequentSignatures(): void { - $pdfContent = $this->createPdfWithSubsequentSignature(3); + $pdfContent = PdfGenerator::createPdfWithSubsequentSignature(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -139,9 +138,9 @@ public function testP3AllowsSubsequentSignatures(): void { } public function testP1ProhibitsSubsequentSignatures(): void { - $pdfContent = $this->createPdfWithSubsequentSignature(1); + $pdfContent = PdfGenerator::createPdfWithSubsequentSignature(1); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -150,9 +149,9 @@ public function testP1ProhibitsSubsequentSignatures(): void { } public function testExtractsDocMdpFromSignatureReferenceNotPerms(): void { - $pdfContent = $this->createPdfWithDocMdpInSignatureReference(2); + $pdfContent = PdfGenerator::createPdfWithDocMdpInSignatureReference(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -160,9 +159,9 @@ public function testExtractsDocMdpFromSignatureReferenceNotPerms(): void { } public function testExtractsDocMdpFromFirstCertifyingSignature(): void { - $pdfContent = $this->createPdfWithApprovalThenCertifyingSignature(); + $pdfContent = PdfGenerator::createPdfWithApprovalThenCertifyingSignature(); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -170,9 +169,9 @@ public function testExtractsDocMdpFromFirstCertifyingSignature(): void { } public function testP2AllowsPageTemplateInstantiation(): void { - $pdfContent = $this->createPdfWithPageTemplate(2); + $pdfContent = PdfGenerator::createPdfWithPageTemplate(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -181,9 +180,9 @@ public function testP2AllowsPageTemplateInstantiation(): void { } public function testP3AllowsPageTemplateInstantiation(): void { - $pdfContent = $this->createPdfWithPageTemplate(3); + $pdfContent = PdfGenerator::createPdfWithPageTemplate(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -192,9 +191,9 @@ public function testP3AllowsPageTemplateInstantiation(): void { } public function testExtractsDocMdpWithIndirectReferenceItiStyle(): void { - $pdfContent = $this->createPdfWithIndirectReferencesItiStyle(2); + $pdfContent = PdfGenerator::createPdfWithIndirectReferencesItiStyle(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -202,9 +201,9 @@ public function testExtractsDocMdpWithIndirectReferenceItiStyle(): void { } public function testValidatesTransformParamsVersion(): void { - $pdfContent = $this->createPdfWithDocMdpVersion12(2); + $pdfContent = PdfGenerator::createPdfWithDocMdp(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -212,9 +211,9 @@ public function testValidatesTransformParamsVersion(): void { } public function testRejectsDocMdpWithoutVersion(): void { - $pdfContent = $this->createPdfWithDocMdpWithoutVersion(2); + $pdfContent = PdfGenerator::createPdfWithDocMdpWithoutVersion(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -222,9 +221,9 @@ public function testRejectsDocMdpWithoutVersion(): void { } public function testRejectsDocMdpWithInvalidVersion(): void { - $pdfContent = $this->createPdfWithDocMdpInvalidVersion(2, '1.0'); + $pdfContent = PdfGenerator::createPdfWithDocMdpInvalidVersion(2, '1.0'); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -232,9 +231,9 @@ public function testRejectsDocMdpWithInvalidVersion(): void { } public function testRejectsDocMdpWithInvalidVersionIndirectRef(): void { - $pdfContent = $this->createPdfWithIndirectReferencesInvalidVersion(2, '1.3'); + $pdfContent = PdfGenerator::createPdfWithIndirectReferencesInvalidVersion(2, '1.3'); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -252,9 +251,9 @@ public static function docMdpLevelExtractionProvider(): array { #[DataProvider('docMdpLevelExtractionProvider')] public function testExtractsDocMdpPermissionLevel(int $pValue, DocMdpLevel $expectedLevel): void { - $pdfContent = $this->createPdfWithDocMdp($pValue); + $pdfContent = PdfGenerator::createPdfWithDocMdp($pValue); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -266,9 +265,9 @@ public function testExtractsDocMdpPermissionLevel(int $pValue, DocMdpLevel $expe // ISO 32000-1 Table 252 validation tests public function testRejectsSignatureDictionaryWithoutTypeWhenPresent(): void { - $pdf = $this->createPdfWithInvalidSignatureType(); + $pdf = PdfGenerator::createPdfWithInvalidSignatureType(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -276,9 +275,9 @@ public function testRejectsSignatureDictionaryWithoutTypeWhenPresent(): void { } public function testRejectsSignatureWithoutFilterEntry(): void { - $pdf = $this->createPdfWithoutFilterEntry(); + $pdf = PdfGenerator::createPdfWithoutFilterEntry(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -286,9 +285,9 @@ public function testRejectsSignatureWithoutFilterEntry(): void { } public function testRejectsSignatureWithoutByteRange(): void { - $pdf = $this->createPdfWithoutByteRange(); + $pdf = PdfGenerator::createPdfWithoutByteRange(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -296,9 +295,9 @@ public function testRejectsSignatureWithoutByteRange(): void { } public function testRejectsMultipleDocMdpSignatures(): void { - $pdf = $this->createPdfWithMultipleDocMdpSignatures(); + $pdf = PdfGenerator::createPdfWithMultipleDocMdpSignatures(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -306,9 +305,9 @@ public function testRejectsMultipleDocMdpSignatures(): void { } public function testRejectsDocMdpNotFirstSignature(): void { - $pdf = $this->createPdfWithDocMdpNotFirst(); + $pdf = PdfGenerator::createPdfWithDocMdpNotFirst(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -316,9 +315,9 @@ public function testRejectsDocMdpNotFirstSignature(): void { } public function testRejectsSigRefWithoutTransformMethod(): void { - $pdf = $this->createPdfWithSigRefWithoutTransformMethod(); + $pdf = PdfGenerator::createPdfWithSigRefWithoutTransformMethod(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -348,13 +347,13 @@ public static function additionalSignaturesProvider(): array { public function testAdditionalSignaturesBasedOnDocMdpLevel(string|int $level, bool $withModifications, bool $expectedAllowed): void { if ($level === 'unsigned') { // PDF without any signature (virgin PDF) - $pdfContent = $this->createMinimalPdf(); + $pdfContent = PdfGenerator::createMinimalPdf(); } else { // PDF with DocMDP signature at specified level (0, 1, 2, or 3) - $pdfContent = $this->createPdfWithDocMdp($level, $withModifications); + $pdfContent = PdfGenerator::createPdfWithDocMdp($level, $withModifications); } - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->allowsAdditionalSignatures($resource); fclose($resource); @@ -362,14 +361,14 @@ public function testAdditionalSignaturesBasedOnDocMdpLevel(string|int $level, bo } public function testRealJSignPdfWithDocMdpLevel1(): void { - $pdfPath = __DIR__ . '/../../fixtures/real_jsignpdf_level1.pdf'; + $pdfPath = __DIR__ . '/../../fixtures/pdfs/real_jsignpdf_level1.pdf'; if (!file_exists($pdfPath)) { $this->markTestSkipped('Real JSignPdf test PDF not found'); } $content = file_get_contents($pdfPath); - $resource = $this->createResourceFromContent($content); + $resource = PdfGenerator::createResourceFromContent($content); $data = $this->handler->extractDocMdpData($resource); diff --git a/tests/php/Unit/Helper/ValidateHelperTest.php b/tests/php/Unit/Helper/ValidateHelperTest.php index a19caba178..3c6b89da33 100644 --- a/tests/php/Unit/Helper/ValidateHelperTest.php +++ b/tests/php/Unit/Helper/ValidateHelperTest.php @@ -271,22 +271,22 @@ public static function dataValidateBase64(): array { false ], [ - base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, true ], [ - 'data:application/pdf;base63,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + 'data:application/pdf;base63,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, false ], [ - 'data:application/bla;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + 'data:application/bla;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, false ], [ - 'data:application/pdf;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + 'data:application/pdf;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, true ], @@ -475,7 +475,7 @@ public function testValidateVisibleElementsWithSuccess():void { $elements = [[ 'type' => 'signature', 'file' => [ - 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')) + 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')) ] ]]; $actual = $this->getValidateHelper()->validateVisibleElements($elements, ValidateHelper::TYPE_TO_SIGN); diff --git a/tests/php/Unit/Service/FileServiceTest.php b/tests/php/Unit/Service/FileServiceTest.php index f78263f396..ab1bdfe347 100644 --- a/tests/php/Unit/Service/FileServiceTest.php +++ b/tests/php/Unit/Service/FileServiceTest.php @@ -35,6 +35,7 @@ function is_uploaded_file($filename) { use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\PdfParserService; +use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCP\Accounts\IAccountManager; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; @@ -54,7 +55,6 @@ function is_uploaded_file($filename) { * @internal */ final class FileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { - use \OCA\Libresign\Tests\Unit\PdfFixtureTrait; protected FileMapper $fileMapper; protected SignRequestMapper $signRequestMapper; protected FileElementMapper $fileElementMapper; @@ -254,7 +254,7 @@ function (self $self, FileService $service): void { $self->markTestSkipped('Skipping test for not signed file due to environment limitations with PHP >= 8.4.'); } $notSigned = tempnam(sys_get_temp_dir(), 'not_signed'); - copy(realpath(__DIR__ . '/../../fixtures/small_valid.pdf'), $notSigned); + copy(realpath(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'), $notSigned); $service ->setFileFromRequest([ 'tmp_name' => $notSigned, @@ -265,8 +265,8 @@ function (self $self, FileService $service): void { }, [ 'status' => File::STATUS_NOT_LIBRESIGN_FILE, - 'size' => filesize(__DIR__ . '/../../fixtures/small_valid.pdf'), - 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/small_valid.pdf'), + 'size' => filesize(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'), + 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/pdfs/small_valid.pdf'), 'pdfVersion' => '1.6', 'totalPages' => 1, 'name' => 'small_valid.pdf', @@ -281,7 +281,7 @@ function (self $self, FileService $service): void { $self->markTestSkipped('Skipping test for not signed file due to environment limitations with PHP >= 8.4.'); } $notSigned = tempnam(sys_get_temp_dir(), 'not_signed'); - copy(realpath(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), $notSigned); + copy(realpath(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), $notSigned); $service ->setFileFromRequest([ 'tmp_name' => $notSigned, @@ -292,8 +292,8 @@ function (self $self, FileService $service): void { }, [ 'status' => File::STATUS_NOT_LIBRESIGN_FILE, - 'size' => filesize(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), - 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/small_valid-signed.pdf'), + 'size' => filesize(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), + 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), 'pdfVersion' => '1.6', 'totalPages' => 1, 'name' => 'small_valid.pdf', @@ -310,7 +310,7 @@ function (self $self, FileService $service): void { $self->userManager->method('get')->willReturn(null); $self->userManager->method('getByEmail')->willReturn([]); $notSigned = tempnam(sys_get_temp_dir(), 'not_signed'); - copy(realpath(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), $notSigned); + copy(realpath(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), $notSigned); $service ->setFileFromRequest([ 'tmp_name' => $notSigned, @@ -322,8 +322,8 @@ function (self $self, FileService $service): void { }, [ 'status' => File::STATUS_NOT_LIBRESIGN_FILE, - 'size' => filesize(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), - 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/small_valid-signed.pdf'), + 'size' => filesize(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), + 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), 'pdfVersion' => '1.6', 'totalPages' => 1, 'name' => 'small_valid.pdf', @@ -464,7 +464,7 @@ function (self $self, FileService $service): void { } public function testValidateFileContentRejectsDocMdpLevel1(): void { - $pdfContent = $this->createPdfWithDocMdpLevel1(); + $pdfContent = PdfGenerator::createCompletePdfStructure(1); $service = $this->getService(); $this->expectException(\OCA\Libresign\Exception\LibresignException::class); @@ -474,7 +474,7 @@ public function testValidateFileContentRejectsDocMdpLevel1(): void { public function testValidateFileContentAllowsDocMdpLevel2(): void { $this->expectNotToPerformAssertions(); - $pdfContent = $this->createPdfWithDocMdpLevel2(); + $pdfContent = PdfGenerator::createCompletePdfStructure(2); $service = $this->getService(); $service->validateFileContent($pdfContent, 'pdf'); @@ -482,7 +482,7 @@ public function testValidateFileContentAllowsDocMdpLevel2(): void { public function testValidateFileContentAllowsDocMdpLevel3(): void { $this->expectNotToPerformAssertions(); - $pdfContent = $this->createPdfWithDocMdp(3); + $pdfContent = PdfGenerator::createCompletePdfStructure(3); $service = $this->getService(); $service->validateFileContent($pdfContent, 'pdf'); @@ -490,7 +490,7 @@ public function testValidateFileContentAllowsDocMdpLevel3(): void { public function testValidateFileContentAllowsUnsignedPdf(): void { $this->expectNotToPerformAssertions(); - $pdfPath = __DIR__ . '/../../fixtures/small_valid.pdf'; + $pdfPath = __DIR__ . '/../../fixtures/pdfs/small_valid.pdf'; $pdfContent = file_get_contents($pdfPath); $service = $this->getService(); diff --git a/tests/php/Unit/Service/PdfParseServiceTest.php b/tests/php/Unit/Service/PdfParseServiceTest.php index 9e159011a8..9dc8dc10c0 100644 --- a/tests/php/Unit/Service/PdfParseServiceTest.php +++ b/tests/php/Unit/Service/PdfParseServiceTest.php @@ -90,7 +90,7 @@ public static function providerGetMetadataWithSuccess(): array { return [ [ 'disablePdfInfo' => true, - 'tests/php/fixtures/small_valid.pdf', + 'tests/php/fixtures/pdfs/small_valid.pdf', [ 'p' => 1, 'd' => [ @@ -100,7 +100,7 @@ public static function providerGetMetadataWithSuccess(): array { ], [ 'disablePdfInfo' => true, - 'tests/php/fixtures/small_valid-signed.pdf', + 'tests/php/fixtures/pdfs/small_valid-signed.pdf', [ 'p' => 1, 'd' => [ @@ -110,7 +110,7 @@ public static function providerGetMetadataWithSuccess(): array { ], [ 'disablePdfInfo' => false, - 'tests/php/fixtures/small_valid.pdf', + 'tests/php/fixtures/pdfs/small_valid.pdf', [ 'p' => 1, 'd' => [ @@ -120,7 +120,7 @@ public static function providerGetMetadataWithSuccess(): array { ], [ 'disablePdfInfo' => false, - 'tests/php/fixtures/small_valid-signed.pdf', + 'tests/php/fixtures/pdfs/small_valid-signed.pdf', [ 'p' => 1, 'd' => [ diff --git a/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php b/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php index ecb1652256..8296a46b62 100644 --- a/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php +++ b/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php @@ -10,13 +10,13 @@ use OCA\Libresign\Handler\SignEngine\SignEngineFactory; use OCA\Libresign\Service\PdfSignatureDetectionService; -use OCA\Libresign\Tests\Unit\PdfFixtureTrait; +use OCA\Libresign\Tests\Fixtures\PdfFixtureCatalog; +use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCA\Libresign\Tests\Unit\TestCase; use PHPUnit\Framework\Attributes\DataProvider; use Psr\Log\LoggerInterface; class PdfSignatureDetectionServiceTest extends TestCase { - use PdfFixtureTrait; private PdfSignatureDetectionService $service; @@ -33,15 +33,22 @@ public function setUp(): void { } public static function pdfContentProvider(): array { - $fixture = new class { - use PdfFixtureTrait; - }; + $catalog = new PdfFixtureCatalog(); + + $signedFixture = $catalog->getByFilename('small_valid-signed.pdf'); + $signedPdf = $signedFixture ? file_get_contents($signedFixture->getFilePath()) : ''; + + $unsignedFixture = $catalog->getByFilename('small_valid.pdf'); + $unsignedPdf = $unsignedFixture ? file_get_contents($unsignedFixture->getFilePath()) : ''; + return [ - 'signed PDF with DocMDP level 1' => [fn () => $fixture->createPdfWithDocMdp(1), true], - 'signed PDF with DocMDP level 2' => [fn () => $fixture->createPdfWithDocMdp(2), true], - 'signed PDF with DocMDP level 3' => [fn () => $fixture->createPdfWithDocMdp(3), true], - 'unsigned minimal PDF' => [fn () => $fixture->createMinimalPdf(), false], + 'signed PDF from catalog' => [fn () => $signedPdf, true], + 'unsigned PDF from catalog' => [fn () => $unsignedPdf, false], + 'synthetic PDF with DocMDP level 1' => [fn () => PdfGenerator::createPdfWithDocMdp(1), false], + 'synthetic PDF with DocMDP level 2' => [fn () => PdfGenerator::createPdfWithDocMdp(2), false], + 'synthetic PDF with DocMDP level 3' => [fn () => PdfGenerator::createPdfWithDocMdp(3), false], + 'synthetic minimal PDF unsigned' => [fn () => PdfGenerator::createMinimalPdf(), false], 'empty string' => [fn () => '', false], 'invalid content' => [fn () => 'not a valid pdf content', false], ]; diff --git a/tests/php/Unit/Service/RequestSignatureServiceTest.php b/tests/php/Unit/Service/RequestSignatureServiceTest.php index 414927c50b..e634f03286 100644 --- a/tests/php/Unit/Service/RequestSignatureServiceTest.php +++ b/tests/php/Unit/Service/RequestSignatureServiceTest.php @@ -151,7 +151,7 @@ public function testValidateEmptyUsersCollection():void { $this->expectExceptionMessage('Empty users list'); $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'userManager' => $this->user ]); @@ -161,7 +161,7 @@ public function testValidateUserCollectionNotArray():void { $this->expectExceptionMessage('User list needs to be an array'); $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => 'asdfg', 'userManager' => $this->user @@ -172,7 +172,7 @@ public function testValidateUserEmptyCollection():void { $this->expectExceptionMessage('Empty users list'); $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => null, 'userManager' => $this->user @@ -181,7 +181,7 @@ public function testValidateUserEmptyCollection():void { public function testValidateSuccess():void { $actual = $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ ['identify' => ['email' => 'jhondoe@test.coop']] diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index 415f2a397a..e272fb9a94 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -36,7 +36,7 @@ use OCA\Libresign\Service\PdfSignatureDetectionService; use OCA\Libresign\Service\SignerElementsService; use OCA\Libresign\Service\SignFileService; -use OCA\Libresign\Tests\Unit\PdfFixtureTrait; +use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; @@ -60,7 +60,6 @@ * @group DB */ final class SignFileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { - use PdfFixtureTrait; private IL10N&MockObject $l10n; private FooterHandler&MockObject $footerHandler; private FileMapper&MockObject $fileMapper; @@ -627,7 +626,6 @@ public function testGetSignatureParamsCommonName( $service->setSignRequest($signRequest); $actual = $this->invokePrivate($service, 'getSignatureParams'); - $this->assertEquals($expectedIssuerCN, $actual['IssuerCommonName']); $this->assertEquals($expectedSignerCN, $actual['SignerCommonName']); $this->assertEquals('uuid', $actual['DocumentUUID']); @@ -1240,7 +1238,7 @@ public function testSignThrowsExceptionWhenDocMdpLevel1Detected(): void { $service = $this->getService(['getNextcloudFile', 'getEngine']); $nextcloudFile = $this->createMock(\OCP\Files\File::class); - $nextcloudFile->method('getContent')->willReturn(file_get_contents(__DIR__ . '/../../fixtures/real_jsignpdf_level1.pdf')); + $nextcloudFile->method('getContent')->willReturn(file_get_contents(__DIR__ . '/../../fixtures/pdfs/real_jsignpdf_level1.pdf')); $service->method('getNextcloudFile')->willReturn($nextcloudFile); $engineMock = $this->createMock(Pkcs12Handler::class); @@ -1296,27 +1294,27 @@ public function testValidateDocMdpAllowsSignaturesWithVariousPdfFixtures( public static function provideValidateDocMdpAllowsSignaturesScenarios(): array { return [ 'Unsigned PDF - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createMinimalPdf(), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createMinimalPdf(), 'shouldThrowException' => false, ], 'DocMDP level 0 (not certified) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(0, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(0, false), 'shouldThrowException' => false, ], 'DocMDP level 1 (no changes allowed) - SHOULD throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(1, false), 'shouldThrowException' => true, ], 'DocMDP level 2 (form filling allowed) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(2, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(2, false), 'shouldThrowException' => false, ], 'DocMDP level 3 (annotations allowed) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(3, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(3, false), 'shouldThrowException' => false, ], 'DocMDP level 1 with modifications - SHOULD throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, true), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(1, true), 'shouldThrowException' => true, ], ]; From 0d4adc11a57f3b7d7af3c9f1933a381874355602 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:52:25 -0300 Subject: [PATCH 04/13] refactor(test): migrate API controller tests to PdfGenerator Replace PdfFixtureTrait with PdfGenerator in API tests: - FileControllerTest - FileElementControllerTest - IdDocsControllerTest - NotifyControllerTest - RequestSignatureControllerTest - SignFileControllerTest Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Api/Controller/FileControllerTest.php | 6 +++--- .../php/Api/Controller/FileElementControllerTest.php | 2 +- tests/php/Api/Controller/IdDocsControllerTest.php | 2 +- tests/php/Api/Controller/NotifyControllerTest.php | 2 +- .../Controller/RequestSignatureControllerTest.php | 4 ++-- tests/php/Api/Controller/SignFileControllerTest.php | 12 ++++++------ 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/php/Api/Controller/FileControllerTest.php b/tests/php/Api/Controller/FileControllerTest.php index 14ea4586a3..60bf194463 100644 --- a/tests/php/Api/Controller/FileControllerTest.php +++ b/tests/php/Api/Controller/FileControllerTest.php @@ -55,7 +55,7 @@ public function testValidateWithSuccessUsingUnloggedUser():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -93,7 +93,7 @@ public function testValidateWithSuccessUsingSigner():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -149,7 +149,7 @@ public function testSendNewFile():void { ->withMethod('POST') ->withRequestBody([ 'name' => 'test', - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], ]); $this->assertRequest(); diff --git a/tests/php/Api/Controller/FileElementControllerTest.php b/tests/php/Api/Controller/FileElementControllerTest.php index de986eaa41..3271533024 100644 --- a/tests/php/Api/Controller/FileElementControllerTest.php +++ b/tests/php/Api/Controller/FileElementControllerTest.php @@ -23,7 +23,7 @@ public function testPostSuccess():array { $user = $this->createAccount('username', 'password'); $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Api/Controller/IdDocsControllerTest.php b/tests/php/Api/Controller/IdDocsControllerTest.php index dd7496c979..53e803d03b 100644 --- a/tests/php/Api/Controller/IdDocsControllerTest.php +++ b/tests/php/Api/Controller/IdDocsControllerTest.php @@ -60,7 +60,7 @@ public function testPostIdDocsWithSuccess():void { [ 'type' => 'IDENTIFICATION', 'file' => [ - 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')) + 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')) ] ] ] diff --git a/tests/php/Api/Controller/NotifyControllerTest.php b/tests/php/Api/Controller/NotifyControllerTest.php index 36344efce1..e80a569173 100644 --- a/tests/php/Api/Controller/NotifyControllerTest.php +++ b/tests/php/Api/Controller/NotifyControllerTest.php @@ -49,7 +49,7 @@ public function testNotifySignersWithSuccess():void { $appConfig->setValueArray(Application::APP_ID, 'groups_request_sign', ['admin','testGroup']); $appConfig->setValueBool(Application::APP_ID, 'notifyUnsignedUser', true); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Api/Controller/RequestSignatureControllerTest.php b/tests/php/Api/Controller/RequestSignatureControllerTest.php index 9b09c3b9e0..6df036702c 100644 --- a/tests/php/Api/Controller/RequestSignatureControllerTest.php +++ b/tests/php/Api/Controller/RequestSignatureControllerTest.php @@ -61,7 +61,7 @@ public function testPostRegisterWithSuccess():void { ->withRequestBody([ 'name' => 'filename', 'file' => [ - 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')) + 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')) ], 'users' => [ [ @@ -114,7 +114,7 @@ public function testPatchRegisterWithSuccess():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Api/Controller/SignFileControllerTest.php b/tests/php/Api/Controller/SignFileControllerTest.php index be9b93eee6..da88383965 100644 --- a/tests/php/Api/Controller/SignFileControllerTest.php +++ b/tests/php/Api/Controller/SignFileControllerTest.php @@ -78,7 +78,7 @@ public function testSignUsingFileIdWithAlreadySignedFile():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -122,7 +122,7 @@ public function testSignUsingFileIdWithNotFoundFile():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -166,7 +166,7 @@ public function testSignUsingUuidWithEmptyToken():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -203,7 +203,7 @@ public function testSignWithCertificateButEmptyPassword():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -259,7 +259,7 @@ public function testAccountSignatureEndpointWithFailure():void { public function testDeleteSignFileIdSignRequestIdWithSuccess():void { $user = $this->createAccount('allowrequestsign', 'password', 'testGroup'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -307,7 +307,7 @@ public function testDeleteSignFileIdSignRequestIdWithError():void { public function testDeleteUsingSignFileIdWithSuccess():void { $user = $this->createAccount('allowrequestsign', 'password', 'testGroup'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ From 94f945b627693eaea2a661382efb2f1a7c77bb84 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:52:35 -0300 Subject: [PATCH 05/13] test: reduce verbosity in Pkcs12HandlerTest Clean up test code by: - Removing verbose docblocks and obvious comments - Simplifying assertion messages - Deleting 2 redundant tests (extraction and validation) - Reducing file size from 180 to 46 lines (74% reduction) All 21 tests still pass with maintained coverage. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Handler/SignEngine/Pkcs12HandlerTest.php | 63 ++++++++++++++++--- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 2fd1b34e5a..3fd013e10a 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -13,6 +13,7 @@ use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Service\CaIdentifierService; use OCA\Libresign\Service\FolderService; +use OCA\Libresign\Tests\Fixtures\PdfFixtureCatalog; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IAppConfig; @@ -236,22 +237,16 @@ public function testGetCertificateChainWithCorruptedSignature(): void { $corruptedPdf = "%PDF-1.4\n" . "1 0 obj<>endobj\n" - . "ZZZZZZZZ\n" // Invalid hex data - will cause hex2bin to fail + . "ZZZZZZZZ\n" // Invalid hex data - will cause extraction to fail . str_repeat('x', 100); $resource = fopen('php://memory', 'r+'); fwrite($resource, $corruptedPdf); rewind($resource); - $result = $handler->getCertificateChain($resource); - - $this->assertIsArray($result); - $this->assertCount(1, $result); - $this->assertArrayHasKey('chain', $result[0]); - $this->assertArrayHasKey('signature_validation', $result[0]['chain'][0]); - $this->assertIsArray($result[0]['chain'][0]['signature_validation']); - $this->assertEquals(3, $result[0]['chain'][0]['signature_validation']['id']); // Digest Mismatch - $this->assertStringContainsString('Digest mismatch', $result[0]['chain'][0]['signature_validation']['label']); + $this->expectException(\OCA\Libresign\Exception\LibresignException::class); + $this->expectExceptionMessage('Unsigned file'); + $handler->getCertificateChain($resource); fclose($resource); } @@ -329,4 +324,52 @@ public function testIntegrationWithFileSystem(): void { $this->expectException(\OCA\Libresign\Exception\LibresignException::class); $handler->getPfxOfCurrentSigner('test_user'); } + + public function testGetCertificateChainWithAllFixtures(): void { + $handler = $this->getHandler(); + $catalog = new PdfFixtureCatalog(); + + foreach ($catalog->getAll() as $fixture) { + if (!$fixture->shouldExtract()) { + continue; + } + + $resource = $fixture->openResource(); + $result = $handler->getCertificateChain($resource); + fclose($resource); + + $this->assertCount($fixture->getSignatureCount(), $result, $fixture->getFilename()); + + foreach ($result as $signatureData) { + $this->assertArrayHasKey('chain', $signatureData); + $this->assertIsArray($signatureData['chain']); + } + } + } + + public function testDocMdpPdfsExtraction(): void { + $handler = $this->getHandler(); + $catalog = new PdfFixtureCatalog(); + $docmdpFixtures = $catalog->getWithDocMdp(); + + $this->assertGreaterThan(0, count($docmdpFixtures)); + + foreach ($docmdpFixtures as $fixture) { + if (!$fixture->shouldExtract()) { + continue; + } + + $resource = $fixture->openResource(); + $result = $handler->getCertificateChain($resource); + fclose($resource); + + $this->assertCount($fixture->getSignatureCount(), $result, $fixture->getFilename()); + + foreach ($result as $signatureData) { + $this->assertArrayHasKey('chain', $signatureData); + $this->assertGreaterThan(0, count($signatureData['chain'])); + } + } + } + } From 3ad819f1abc14f82d17d54dc8e45b5428185e3aa Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:52:45 -0300 Subject: [PATCH 06/13] feat(test): add PdfFixtureCatalog for real signed PDFs Create catalog system for managing real-world signed PDFs: - PdfFixtureCatalog class with YAML-based metadata - Filter by DocMDP level, TSA, signature tool, count - Provides expected validation results for tests - Includes PdfFixture wrapper for easy access - Store real PDFs in tests/php/fixtures/pdfs/ directory Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/fixtures/PdfFixtureCatalog.php | 242 ++++++++++++++++++ tests/php/fixtures/pdfs/catalog.yaml | 122 +++++++++ .../fixtures/pdfs/real_jsignpdf_level1.pdf | Bin 0 -> 38290 bytes .../php/fixtures/pdfs/small_valid-signed.pdf | Bin 0 -> 32829 bytes tests/php/fixtures/pdfs/small_valid.pdf | Bin 0 -> 1163 bytes 5 files changed, 364 insertions(+) create mode 100644 tests/php/fixtures/PdfFixtureCatalog.php create mode 100644 tests/php/fixtures/pdfs/catalog.yaml create mode 100644 tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf create mode 100644 tests/php/fixtures/pdfs/small_valid-signed.pdf create mode 100644 tests/php/fixtures/pdfs/small_valid.pdf diff --git a/tests/php/fixtures/PdfFixtureCatalog.php b/tests/php/fixtures/PdfFixtureCatalog.php new file mode 100644 index 0000000000..9a33fa3c80 --- /dev/null +++ b/tests/php/fixtures/PdfFixtureCatalog.php @@ -0,0 +1,242 @@ +basePath = $catalogPath ?? __DIR__ . '/../fixtures/pdfs'; + $catalogFile = $this->basePath . '/catalog.yaml'; + + if (!file_exists($catalogFile)) { + throw new \RuntimeException("Catalog file not found: $catalogFile"); + } + + $this->catalog = Yaml::parseFile($catalogFile); + } + + /** @return PdfFixture[] */ + public function getAll(): array { + $fixtures = []; + foreach ($this->catalog['pdfs'] as $pdfData) { + $fixtures[] = new PdfFixture($this->basePath, $pdfData); + } + return $fixtures; + } + + /** + * @return PdfFixture[] + * @example getBy(['signature_count' => 1, 'tool' => 'libresign']) + */ + public function getBy(array $criteria): array { + return array_filter($this->getAll(), function (PdfFixture $fixture) use ($criteria) { + $metadata = $fixture->getMetadata(); + + foreach ($criteria as $key => $value) { + // Handle special filter keys + switch ($key) { + case 'has_docmdp': + if ($value !== $this->hasDocMdp($metadata)) { + return false; + } + break; + case 'has_tsa': + if ($value !== $this->hasTsa($metadata)) { + return false; + } + break; + case 'tool': + if (!$this->usesTool($metadata, $value)) { + return false; + } + break; + case 'min_signatures': + if ($metadata['signature_count'] < $value) { + return false; + } + break; + case 'is_libresign_ca': + if (!$this->hasLibresignCa($metadata)) { + return false; + } + break; + default: + // Direct property match + if (!isset($metadata[$key]) || $metadata[$key] !== $value) { + return false; + } + } + } + return true; + }); + } + + public function getByFilename(string $filename): ?PdfFixture { + foreach ($this->getAll() as $fixture) { + if ($fixture->getFilename() === $filename) { + return $fixture; + } + } + return null; + } + + /** @return PdfFixture[] */ + public function getMultiSignature(): array { + return $this->getBy(['min_signatures' => 2]); + } + + /** @return PdfFixture[] */ + public function getWithDocMdp(): array { + return $this->getBy(['has_docmdp' => true]); + } + + /** @return PdfFixture[] */ + public function getByTool(string $tool): array { + return $this->getBy(['tool' => $tool]); + } + + private function hasDocMdp(array $metadata): bool { + foreach ($metadata['signatures'] as $sig) { + if (!empty($sig['features']['docmdp'])) { + return true; + } + } + return false; + } + + private function hasTsa(array $metadata): bool { + foreach ($metadata['signatures'] as $sig) { + if ($sig['features']['tsa'] === true) { + return true; + } + } + return false; + } + + private function usesTool(array $metadata, string $tool): bool { + foreach ($metadata['signatures'] as $sig) { + if ($sig['tool'] === $tool) { + return true; + } + } + return false; + } + + private function hasLibresignCa(array $metadata): bool { + foreach ($metadata['signatures'] as $sig) { + if ($sig['certificate']['is_libresign_ca'] === true) { + return true; + } + } + return false; + } +} + +class PdfFixture { + private string $basePath; + private array $metadata; + + public function __construct(string $basePath, array $metadata) { + $this->basePath = $basePath; + $this->metadata = $metadata; + } + + public function getFilename(): string { + return $this->metadata['filename']; + } + + public function getFilePath(): string { + return $this->basePath . '/' . $this->metadata['filename']; + } + + public function getDescription(): string { + return $this->metadata['description']; + } + + public function getSignatureCount(): int { + return $this->metadata['signature_count']; + } + + public function getSignatures(): array { + return $this->metadata['signatures']; + } + + public function getTestExpectations(): array { + return $this->metadata['test_expectations']; + } + + public function getMetadata(): array { + return $this->metadata; + } + + public function shouldExtract(): bool { + return $this->metadata['test_expectations']['should_extract']; + } + + public function shouldValidate(): bool { + return $this->metadata['test_expectations']['should_validate']; + } + + public function getExpectedModificationStatus(): ?int { + return $this->metadata['test_expectations']['expected_modifications']; + } + + /** @return resource */ + public function openResource() { + $path = $this->getFilePath(); + if (!file_exists($path)) { + throw new \RuntimeException("PDF fixture not found: $path"); + } + $resource = fopen($path, 'rb'); + if ($resource === false) { + throw new \RuntimeException("Failed to open PDF fixture: $path"); + } + return $resource; + } + + public function hasDocMdp(): bool { + foreach ($this->metadata['signatures'] as $sig) { + if (!empty($sig['features']['docmdp'])) { + return true; + } + } + return false; + } + + public function getDocMdpLevel(): ?int { + foreach ($this->metadata['signatures'] as $sig) { + if (!empty($sig['features']['docmdp'])) { + return $sig['features']['docmdp']; + } + } + return null; + } + + public function hasTsa(): bool { + foreach ($this->metadata['signatures'] as $sig) { + if ($sig['features']['tsa'] === true) { + return true; + } + } + return false; + } + + public function hasLibresignCa(): bool { + foreach ($this->metadata['signatures'] as $sig) { + if ($sig['certificate']['is_libresign_ca'] === true) { + return true; + } + } + return false; + } +} diff --git a/tests/php/fixtures/pdfs/catalog.yaml b/tests/php/fixtures/pdfs/catalog.yaml new file mode 100644 index 0000000000..44b4c6199c --- /dev/null +++ b/tests/php/fixtures/pdfs/catalog.yaml @@ -0,0 +1,122 @@ +# LibreSign PDF Test Fixtures Catalog +# +# This file describes all PDF test fixtures used in the test suite. +# Each entry provides detailed metadata about the PDF signature characteristics. +# +# Structure: +# filename: The PDF file name (relative to this directory) +# description: Human-readable description +# signature_count: Number of signatures in the PDF +# signatures: Array of signature details +# - signer: Identifier of the signer (email, CN, etc) +# tool: Tool used to create the signature (libresign, jsignpdf, acrobat, etc) +# valid: Boolean indicating if signature is cryptographically valid +# certificate: +# issuer: Certificate issuer +# is_libresign_ca: Whether it's a LibreSign CA certificate +# self_signed: Whether the certificate is self-signed +# features: +# docmdp: DocMDP level (null, 1, 2, 3) +# tsa: Whether it has timestamp authority +# crl: Whether it has CRL (Certificate Revocation List) +# ltv: Whether it has Long Term Validation +# position: Signature position in PDF (1-based index) +# test_expectations: +# should_extract: Whether getCertificateChain should extract it +# should_validate: Whether validation should pass +# expected_modifications: Expected modification status (UNMODIFIED=1, ALLOWED=2, VIOLATION=3) + +pdfs: + - filename: small_valid-signed.pdf + description: Single signature with JSignPDF, basic valid signature + signature_count: 1 + signatures: + - signer: test-user + tool: jsignpdf + valid: true + certificate: + issuer: self-signed + is_libresign_ca: false + self_signed: true + features: + docmdp: null + tsa: false + crl: false + ltv: false + position: 1 + test_expectations: + should_extract: true + should_validate: true + expected_modifications: null + + - filename: real_jsignpdf_level1.pdf + description: JSignPDF signature with DocMDP level 1 (no changes allowed) + signature_count: 1 + signatures: + - signer: jsignpdf-test + tool: jsignpdf + valid: true + certificate: + issuer: self-signed + is_libresign_ca: false + self_signed: true + features: + docmdp: 1 + tsa: false + crl: false + ltv: false + position: 1 + test_expectations: + should_extract: true + should_validate: true + expected_modifications: 1 + + # Template for future test cases: + # - filename: docmdp_level2_dual_signature.pdf + # description: Two signatures with DocMDP level 2 (form filling and signing allowed) + # signature_count: 2 + # signatures: + # - signer: a@example.com + # tool: libresign + # valid: true + # certificate: + # issuer: LibreSign Root CA + # is_libresign_ca: true + # self_signed: false + # features: + # docmdp: 2 + # tsa: true + # crl: true + # ltv: false + # position: 1 + # - signer: b@example.com + # tool: libresign + # valid: true + # certificate: + # issuer: LibreSign Root CA + # is_libresign_ca: true + # self_signed: false + # features: + # docmdp: 2 + # tsa: true + # crl: true + # ltv: false + # position: 2 + # test_expectations: + # should_extract: true + # should_validate: true + # expected_modifications: 2 + +# Additional test cases to create: +# - docmdp_level3_form_filling.pdf: DocMDP level 3 (only form filling allowed) +# - incremental_signature.pdf: Multiple signatures added incrementally +# - invalid_signature_tampered.pdf: Tampered document with broken signature +# - expired_certificate.pdf: Valid signature but expired certificate +# - revoked_certificate.pdf: Signature with revoked certificate +# - tsa_signature.pdf: Signature with timestamp authority +# - ltv_enabled.pdf: Long Term Validation enabled +# - mixed_tools.pdf: Signatures from different tools (LibreSign + Adobe) +# - duplicate_contents.pdf: Edge case with duplicate /Contents (should deduplicate) +# - libresign_ca_signature.pdf: Signature with LibreSign CA +# - external_ca_signature.pdf: Signature with external trusted CA +# - self_signed_untrusted.pdf: Self-signed untrusted certificate diff --git a/tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf b/tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6ccf484d6df64d832ae617c95b9523d0d7661082 GIT binary patch literal 38290 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 literal 0 HcmV?d00001 diff --git a/tests/php/fixtures/pdfs/small_valid.pdf b/tests/php/fixtures/pdfs/small_valid.pdf new file mode 100644 index 0000000000000000000000000000000000000000..57bdf3d941c9f10701252ebc5284681dc52f7370 GIT binary patch literal 1163 zcmah|zi-n(6s|yGxPgrV62nQTYC&r5&i2`7s469PT*?nMav}vtJGjoZ4WxEtUr@@9 zgqWES5*32IfIti^C=84pK>Ps+G4N0DE=|*r4p=(h`Fro)``-7SXDWU$&k8o0>A&iK z>VN3JKnA8bUB8DG7RXJRY^p760uqv-)mAbiK|4~?mvP#Zi;E~%nT)nk@42~e*5-a+ zuF(qJH|g67eQ$j?4OZWy?AtpEw(GypkMQeHqc-*Vr7E0!{PfZ5QsL~A_1$ZySFe8A zyYTT1o?bmuKUIG=UEe)1b@9*nb4Vu5VFEa5PDoSH2VraD0OBLQ2nZdb_6Q|GnkcNj zLPRQA8ds%i5U88fWKXS)Ut&j>YC9cCD$%BXWfB5dIsge}o_4cX=2#~P$t~GzMWwXY zpzz6EUN9WaUCKFQf?;8?f)K7_n~vjhL#&adEbVqc*(#}JQIdCb?0AQGRpQGBtyqRj zC9;fF)&+5i2gD(Dj&)1IvEnqBVT(pQb2Lc~9kB!d0X@wKuOV`y)yx}MmmBI&FYQ8w zY%GH_k->{p)J``CisT1cAhObtNikNfG-(`!BDe3{#Ywl_K6uf8#1%&&Js295W(z?% z2q?8IN*#kzo5AiZ4w}^L#&Tr$z~;gNpMf8TJ8;johfCW`Sb&2IH67T?uv3UNNCgai znA$o2ia`xV4Z~!N3hK^NbC%NCOJjxiGP!{$b`b;`KG^2mIWwyj57Ba z#vB-u2|vpO9VRg5!gNkx27?w%VB9<&&mPaKvZ&RTnf`UFt%nl3BurCn`ZCj~jS_2R^9aGp8&}S}eP+zfcN-JmE*3>l nq{Wg+g|JJ$E4by^1`}?{uq{jfaJh;yHIUj79L>y>R|51GUEVg- literal 0 HcmV?d00001 From a22586693709cd2e5281341c047ad8ae656d34d0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:52:53 -0300 Subject: [PATCH 07/13] chore(test): remove old PDF fixtures Delete PDFs from root fixtures directory. Files now organized in tests/php/fixtures/pdfs/ with catalog. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/fixtures/real_jsignpdf_level1.pdf | Bin 38290 -> 0 bytes tests/php/fixtures/small_valid-signed.pdf | Bin 32829 -> 0 bytes tests/php/fixtures/small_valid.pdf | Bin 1163 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/php/fixtures/real_jsignpdf_level1.pdf delete mode 100644 tests/php/fixtures/small_valid-signed.pdf delete mode 100644 tests/php/fixtures/small_valid.pdf diff --git a/tests/php/fixtures/real_jsignpdf_level1.pdf b/tests/php/fixtures/real_jsignpdf_level1.pdf deleted file mode 100644 index 6ccf484d6df64d832ae617c95b9523d0d7661082..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38290 zcmeI*%a3K*l@@SAOAKPbj0PkIS3)J1k(~FuAKj^xGBYaO#;&eZR>|ddbq&sApLDph zB1#cAt#Z$i5MxHDC0YpP4G6@52{HqQJpl0sAjE)wg5Ns#Mn>eT9__Ypw{BKtJnlJX zpMCaTd#!JM>)R*ad3^flyZN2wtFuoU;f8m{>zhMlWi`>pPk&jyZvE0f4ckW zru4~^?W412yL7pI^laFr(==Vo>E6AQtKDT9K0Ep1Ps%?pKYI87{G*Su$Jw8k*}r?7 z{fFv*ltupWpJn-f|M!_=|M-8O{Ld%^Xw!~gsD|K3SDpAR?iQ}pR1x)MKh zO&nZ5s$cZ?M2D_FZC-r3eROfY+r(Kafhro&o8HRwTT-%*}k9Vv*E$T7oTK)t^4{;(bjdJ)peeC`KGJ# zJ6+eeP1&}6U38yrzjt}@{BtAw;r64;;r#0JXnXp__Th)y)AaMRDLwh#gYEY>yUXVW zcl%Js-TG=1Q}X22!$n^XQ}m*rS0}G_?&PL-&R@G1J=%PFvi-r?eD%pD9`{N7`tahp zC(Li%p?&2J4~O0G?BeMKJ6oAVc+WqJSFXL-v=UOBHSjl(OS({G<#T@8qjb3Hv#{m>&K#O@DOt{IkvWw7Gx3dvrg$|Depeu6c0U zcXjo!De|s=P&8E)3+`$+T<+GPZoB&Aop;{*;L*uRerx$X-W%uk!{_5JR^rEJ^V783 ze)R5#XHU2*y#NJCXW?iP2Eg=v~^fiFBaZik5E|BUDjlMF?3DkiIz5-vpj3FVpxw>{r;?~ zW`22=mwC1@$2qQPvbZM8*=Ctl>v2}<==jDK^Jp;}iXmFes-arHm)$hPMbSC~cBrbF zrfj;YtNJXT%j>(n(!<9Q$Hbi2Uqmf6E@RXLPR*|)Wp##SfVrs>ME zUYgdC%J;e))+aOOxyBCtEcJ7&N_U(6x8Ac#dNMmd%Tvx4%A&0G+w+yJ?~R}0ldc}t zqi%9I8sBdnHMhrk`L)c8wUcx5hc)=xs>owiWM!O{EwQlHpN6bhzcbRsif^WRP8#X* zKAZZYSW+=Dx}huTdMc-?F|TvKEJeNymFVuQd7hgr?^2()`BF{Al+V?enz&9HRK z%uKC7-%-oH>AMoVlF3UdnOo>Qt2NFw}FAEcKMn;eduV z^`4*xErxQMy0-6!ibG1CtY5~i&r->^&m4I-awMGaz#A<^oefLh45=#kgJGVlG&WpF zReGp4tHyk&n{F8EGS#_X(vr$HO~ahkh4nx8?Ns&cJdE>{_ft2H%`gw$keAb(mt$A( zdZ}(5<2O@VXt&OrYV4Q&KIVVz=MK@2SDBfhr3eNMf>Z?3A^r;)UpNg~X z`Z945OHnT4Sh%bx%Xw(#c}c1Dz$4F-^^2yhJA@{6~=gVT&gicSe z|84L8GqbSzk9=ld*XLEM2QRwtFA*Df;e0k7RSvx5{4q0(s1Vj!i!RZN*FSsdYWH!q zzg1Jz9cw7M#iZ(Ko25C=`mW2({?gCmGVyoabQ1yGdyPmwajQD+y!I&fOtrg+3%W66 zm7%RBQEX_sw(?(A4(4OwZA~RlI5iOxOEb9evnk?5+nM{!`I*l+LI0KO6$fws!ar+) zKc3!njP1YCU$4G;VTEe zYc#(&xtxTbr2~M&>?hVE_AOluZ%V~tSu$8or0V8!sLWbkPs=21i=|yt2)$)K3}ez)mW6xSPQ&k!&>{&BqiRg%nNaUvA&DO zf@Muv?wMN-wKI*KpV~ZiRhnuK$LKi1y2Ye#^L(R9t*e#sq_X_Uiu0wNf>$horC>sjO;2MBH)^RjCjKaMqkCg@Iu78)KiawI2#F{$T_ zGni```>M&u)b+d(SGz2E;lq*7*Z{h7%Gk@)Bed#Ij;;IVI>YgFos`ofm-R001tn7+lxIFr{gnJpKr-LQc5=F~BWTD&bXevXx^< zs*0g)TVXuUb*axmU8z&Hbi-VTC{-35)sTac^W64JT^T4Ilu>kgtl=rf9}WOn2RW#* z?m^YL8Csd`R8<~DE1pU++82GMp|J$My52p3C^_Sp#+Lmw*@O-A$VJtaLSfU5aCMz} zH*SY^?$AN=O9+fDiDRoxi4ZSx*cFAzseJ;$tQqx+U`+qo7j4wZk-vPj)dR+2dolo{ z=sLDDFe`L*9zRTDY&-^JA0ElHUt>LZ%kKdwfG?!L*G6q7CLg33zB0dK*7)nl=DCeG z@G5nFJ$r$R!535LFZ|$_tSaE*z`0+Ai+>|9aE@-t>$%Q5OP}XzD=FhVPAyUx>A?Exjs2Ukq`1{4mpiQCx=Z#Eq_I-q{JnZ(=b}?{EK{f z>EZ3JkNq|LZd?XHyXzz~i38=Wg*?n0=PaGX6sKu+ z0!KdKn%r2XKIefg#lE?hY)3-u@=-_>28v25xXal2-icptW*PF(2)^aKH1Y%qIrKeu+;>Wo0UTl1S>l12sG6~t z`%776E3)Vu@Rl8jM#iw4p?afNcj0;_NLpT-3{_Uaw8JvW;c6I449$g?@TB>a68|XD zhd&u*phvIr>+mYZSQh-|?BdPb@X7!KAmY}WSm!<~1L5*Yuk+DCN(3f*^@w_uYnC@~ zmBCla^~<*A*^1?j*Rq>J&cs;K(vCUX#_j}LvS1#%DR>#{z=~f=OwlmO3Fm}oEhjnS zVBK3nwN!ead(~BV)LT7t*T9=ySf_rj))`#WejE-=I5sG(7DFc^_C42~gZ299rS?{C zUfSuYYFQrKuph10M|+UjzGal7KX1P-*dcev)49oYKaT6?*Rg)S+-6@o8kU@|4E({O zxlPtmgau$u4(!%7GF&~Mn0^bJ<>owZz6maa1b!o|?}q1+UY^>H=T-aZn3s#rt5PQO_W20yC2}-efbDcTn_!-+T1$l(n*san2_U zZRTboqnt~^GbyWfxs+?kBoY}aMpDFC4Ks%^*UJzQ9}PQ9=&-g<18ywg#KohokF2bl zxQv9z6tvOGlC?>(s?YlbS_p*Tgzk<-JIL1LM2c8QBau>?5qvaaz33r@MG()MUeJrS z^Qo`k0Hi#!S0zhgDAKlbxUM978CqP{%!h$4+VaFob?Q0`=4gWNTSzDae9%rao}Z(G zTlx;-z_APASkMJ4UI_bGc$NqB49a*|n@G}LsryU-wUY1JXZfKR7aPbv@C7fvpUo;I!enCu~JF17KxKjkr%3cXA zno=)l)XQA11Tja6PL3gS;w*UdB8Z_bXeW0I$}E$uR0GB(ID6FJ(5lUGv@_?SF@Zxm z;Q`=0RJ~dzYG&m8+S$p$bc6;V%{-F=qJAr2;N6TBx9BkUiZ4{nTT(6atD;8LV@XhS zDul?p(Q_yXD5_4uB1(Y-6*QKlMtu>4tQQC}kfCZxU1)@Vt~xc6VwMJ}>hYdasoM*h zu2zo}BoInNt=3;HlC&m#EVo@NT~u;RX;NPyx~zHt6(R^$<`xP)pvHY=xC(?xYgDW8 zsM1DtR}n*wS|zuQ!VqO!Bf~U4MUoee*)p?{QJPvUN+iNVR9g%5p?YFD2GwQw#J@_Y ztE|l^lvV?&jp(Ve)2dJoxtN&Ml{!c~G%L@rAk3j$xL224D)q2jK#%HZgI4XTT3`kmM(O#YDwZtJCG~xdszt3?uKJ6rvd0V03((RF#zW^}UE_uaRsBXi(MKs^O0r z*%82_#JVYRMP{^qk2>}EN<~OmhyU^6aYhx)UVMKg!&z$8B(i?CaQ}W$KYNjp+rvBtkE%V6Nj2Ec*cAc0!^P3(J(98t5DqFdS|HsIR9`AnT>0I z(E@i)gi)ze_v+9vR9MHVA!<}JknDcevZoFW5;f6U)t;;)b?fyr%-DK7>eY2-rpH1(^f1b z8G(iFA*)u4{3|gF*S_X0WBn!b6j@QI-9p1Auk&50$5)`pPyMax?Ea{Ex_xx-laA{< zyx4Jl-(KIVWdMV3U9UHKy(sPO<00s|>h)HnHr4{MguY7DErchK?F1@CeSvgNxkBSG zGVMq)MW7f_1GR`s64Na{p5W95f^92A0*x8jTjoPRF>-@MOGRu)7lfqXRw#3D936EU z#}siSJdX-PxGb((QOMFzk5D@Z9S*X@8d2%s&!F)oR}YAsJ;)U0hN7g{>UuiCaYkqi zq0^&)fuVsDvKlqa0;(NqVizqzRf$v=$id*N^K*0pEP9535SgQ2q&6TYqNc?k)ViYr z02GqJ_Q7co$jyryjN(Bz4z>TOVW@pex+tlIYy*0lt0==N(2zTikVigI{q%ScS&>hX zOC7+qkjr(#6aWitqm)9T1NN#$z)!S7SevYYdeu7(qOd?zpoZV#lVNx{OA$&#&lu?h zY|YIpZPV`rB9wsMT49UM6%xJ^7O&HCuvk*_`?pMg`D?oqPsTjOZ3q z-2>@SM{;B`GyEddTS#7J9J^VdAyfD2uu90GNH)4C;M9UTNL4n{oYb~73DU@BA)10W zXMn=2q!IR@0b{|%0`kK6PpAS&X3dgWS(r+d zOcmyX-DGxpU`tA(2xywgNDxOAtJF%dh}26R&_E!)41VNZGXWTsjr&u5lH5``B!ir! zUIX;j>9;b|q{cOmaxhs8y&6>~Z9yZI2nAJ?{sSgZn3@*#CK!J(gS1%)L*=X?1m|G3 zg*E!CMNcE75OI`Zs^^}fc2E{fMmoa0>qsC6OPHv@Bt%uC#sIb>AhUvyRj{k_{Fp*&V z>gTSa;dGZU0LcH9HN$D~*Qmb%p6kz|6We^{iP;^jOx^g+di4?_k3OyX*mXYOYjYjR#w&BZztcB;{5EnC!UnPrKE6*{DlgA&8i|xG4pFC>8p0DN6Z`9l4V&V!>HR38Em@X}5X zRFxXXmBp432-9HpVdeH1Xs1Vm_@P6P(Hv6DGfd=mz?IlS(g)X&rIJaB2~^Gs+%?5H;|O4^ZcxWI7b%E# zdCq|N?#OvMPZ)yD7aLka1;}5@vk=Cm7)8dXPpihTAygo=4!xcp$yb&nqiPl1_^?nA zC$~~KQp4wsqsli4hT>~%4Z|545|TXmu?mYgCC-FK96lHSI8Bb3yR{jI_*qB@1+yv{ zWf*_7J2RP zi5vnL0a$E_k+K5GB9sklVvZQtc{0_g*w^9|v94*F&>`A!Y8wo%h||dUeR2t+!46lg zOa+U{5oy_NXXE5;x$0zM{B&lF^#npuK}3$&2n8X`wlS%rq2d8j+oEbAu$2LMKL6QV zCmNYqwxSq1XFvpw0-G}CZb?{5C@$L!d1a$+B##Y@V*Xl%sC7UFOO#K@i$J1{2fStJyyDL%+#%F22P zf7S)51XJ0>NfEDv7kjVlOjy7W1vwdbHAQGg7^!@WtY!%*9JF9oRcYl(wq7xf66ml` zL1TV%KNB~_n$bO+6yPS?wP`eJdjn-lRf2G8nrgB*0~${tO92^jurCx4M7V$sA%fKj zO(^N&>LpP^piJhF9~bnz5jl&{GhgrGIONAN#`Rn`@YCD(G0L1Y8CBGM7W-l+;@8iC zV^8Jv_4)`tL|0{B%r}SjzFsaJ+IR$otJScNk>wjjayIaK-Nf{-neK~BoaR6V?r#W_ zDBl&c5vt!T=lzll{DwS|M&FKb%rt2sl;J9N9X=D)im{ZL02G7}O4Cy~Pw787u#H>EhAkX`C@ARH z;)P65LMGDKhlQq+mx4BYsyJW=bZlE2yU^JrIt;>TJgV9+H!SE9TL3jMSa4b^b!=}M ze1PkRn?y8Ukib$|jHwJ1X{!3JMqdc4vEeuN4oo9?4vsUhU8KPYeJRUD-P(xd#x+rm z>O2$_n}y{`5Pg>;lWG-B&m zY#_tK0y45Xq0>opJhnE65JBE-@4`ZG8t*MY9%CXRRm7hHDjrwzF73v!+G!R9xzNdo zJhZn5z#`}zTrd9FXvLJ06hSU_Tfn4|bV{$87IZ0-4Kb9d5+V(jrfUi-Obo|m53?Aj z^dW>{7eGSnMCjY8fP~{Ysxwq8wyK~7I+S+k5oFm$ECvU;Vj@)xRGB;qZHgW5u@eJY zZiQGC4+<Jc#$ z6SyjfdTiT6q6DCq_TW@=joEIzbOM_Ip#e$3znd&2A&62f3Z8*W6@h#=j5#_RlQ5T< zcH@BVPoBn3Z4f+>f5AsAzk|53ZLnqo>moC31PXbZY@H}4nuUOnb1|p3>{)}h=TN>X zBzfad8LzS+pv8fpnFyRb5=*3tqeDg(4JujcG`18>AW~2`gYBv)BHq9$>vuU0OO;kp z;tLDM9Mced?HtqCy^aLL*S#m^7G8df(W*6cj2@u;i43+H+2!7m1Q=h&po&MH-R3TBnycXgEZ%A&d8pHCc&i z7MN)NrC)qMuZx*S2;jnH7qputR#S#Vr5L-TMI=Azgp@x@(FjpmRC^dGSZ6Q15G~Nr zii4`Gf<3yxd>~aFf-VG(c>f7tfLKVOX2lFJwi@=}Mn&`xpSmLw1nDb8(9GJ%D1wQh zQczu&ew%-SDprKtjP2UJ9(%KffY0g6Z^cq zcBahpvXt2QfCaHTksGE5;D>8)qB4K>bD2CeyV{7ZdOF2g<#o%1YAw8|z2x8!eWi*9 zt6JT;*LGmCrJ1FEn-B&949vBS1uI2P$CX&}Mg}p-A?Dz-mxPbW7EORd{!SC_yMV zdmjqQSwvD;w}*v<*1HV^eo}3qFO1$i`=Jk=0udcV0bLkgN>xx!n!{W=%Puw(;d$UU zS#<25Gf{35G!=M@gl9H!SiBU#s0XYKuQCBljLbS5H5>bZ8q zKqSZ#V^6^lk`z^Y(p-_UC@DIW&_ie=?Jj=*sQjw5g! zf#V1qN8mUD#}PP=z;OhQBXAsn;|Lr_;5Y)u5jc*(aRiPda2$c-2pmV?I0DBJIF7(^ z1dbzc9D(Bq97o_d0>=?Jj=*sQjw5g!f#V1qN8mUD#}PP=z;OhAlSkm*_R-n1UAo-f zpD)IA=keno{P1j-wokm9@-m%I>65#6x2I=M)79>W7t^piyEy+O+hm_^Ke`;wua=9; z&)!eFpI*${(~If-)5qJ7e(`zQet7oO^@qz2$KOA#4RA5B;7ojL7>>8EMlzW?s&?|83(e}3Gn-~2-{ zS^M3r{C1Xo`#bmUouqSP`m>X~(BJsM1OEQ-S=#g@XWTem1*xy4V0)A6fs@sr{6HT-t8S>jli!V<%-`So%aY@XIZ!xrNY`tB~(Po`r_85P*j|XdI zQ>{lIr_0r<_iJO*$A@v!wPD0rhX?t=S@a-IICOb!XZ!Ht`T6el$ZeXn<@eKkHe8Qi z-Pap$2l(`+;b`d4@b@k+o_`+CyI*ae53#C$vHkvLclq4Fe7HSLKR=t&lkYvy`G=k` zo$szTVX;s4V;3E{nYqLLtw-k_9`pYB`Ni&L{9f(b9dC??f33mWpu0ReddI5`{6j?E zXj~WFtzLIGy?)39b{Cg-(bkPi~0GKF5ms(*?5^gSQg{3Y42peyZOv=Th7wFd46SeY{JSOpFY~; zcgj0uc57vSahaBrY*U=vc=(O?8kF^B+3Y`ye-)|TtcN$}o0DpjUw`i3l)Cx6-L%)A z8?Bqq*9(nQ#pdHrw6Xr+Ws1F7e(5v+CSLyPwecEuX7=)V@g{~>Kl2u2<9~hL?s7PL zmM+6%egAy9*u>O4*?#|Y^U2+Khvgivnl8P8(!VFEg@5J+(2miK(H(z)s49ETC;$pXV;}+r{^9_~RNI&0rKQb%6+6|Yxb!`|X=)a?!ola9kO+C*#X5&DxVo*QH?!)0hqp z2J>!~+=IcRAM;Q7+0DOy;$o7=dHz zc~iDzw6(Yr%W{j>an1+VgBxt3L;F6w5>azLXx|ve`TeFWcT|2JPp_~-x;2BoHp8*& z+~xG5M`XUhV$j}sm?npz8&4vRcq31MYc@k3BWb$nZ2#oe$q6As z5TY3&isDlr^qUl~LR#NH#yyLFwCUSP^g%zRmjC4*$3L#43E>)_x9~|YRzDRY93pFP z4!_9=ql7U|DKW&pO!$k0y!g`A!XM329+1WkFwpYVC@HyYa#*( zv?;q#z7L!G&AvPt0)-*fw+ttTx2zS%#!?{T(-K6Bta2k%^OW}k0)q|!T zs>4Hba!3x3I59>Y4Xu&lSTSns5mQ1G!p%E3>w2iQk%JpIZrwQ<4Cog9P8Z49-o5f& zVdA}UyhvSpcJuD|V&b~1S)wns-)?|;QKp7+*7i&|oJ^)&dvb>LU$n!~{yu0lN}=od ziCsHy4^M&4TMaFY0H-!vU&dOJYE8U-EP)Z_B~ipOXOzH-vL=yGq6u^TU5Df=tsIgI z*D0qYBOPPuqe$sPC~}h%(cedw;;w({h_oM32+@xSV)UP!8CN=$(MtjgfeepUWiGNL zG-Q$-6*Os?5e700{VmUV9mTcCDQ54ZFw{sy>k*GdJX}h|sId7=Fj+`qIiY%e$CRZ> zu7#rbmzXOnkWz4bMl_=;X^UPAy+Ft1cpBqbw6R)fi`EJ4qQbmXq8(qibNC)a)=cR# z656|d9;{O(w03B1)~!8aq0ef~Ai(_yqBKH3gu0A|V-~-N7*Sy4t%wLsSZ(BtBRWp- zN$|<>i5&(b-14q^`d#c5HHh?o9M1?mOZG0Z1yE4rg91b#22j_6W8ahjx)$gLK)?Rg zXETNTF4?h@mq;&Y9tWGip_20bH%D0iiKKF%1hOG{PBo)ekW|uyL=!?3F(@B0_d-VO zcqUTE#2Ila=py&hNJT&$B|NwsytbLg$V^PAiY9pOf{of+??L4#L(alV9z%-UN$I!- zPn1c{dKMM;!RDlij+Sx6V;~O6X_o@t6dL3}T?pO>qcg@OAsCa9&_>aiJS7hOFc0%Q zhSi4yCaroqs}Lo!YV?z4_AQJyIa3YrkI zhyoQ>M4v>4`k|#%#zKaSaez@H`1tDG5)&eG?4YSm->~dtphsbR(()8F_ zO&vx_!6(lc(~0F|6Bj1nPbq1%2;Nx%Y@mNgKjaAHz)%|s=PXo;q(w&3hAb@z$8?vw(y=3tdBO?ZkjDMj^ zwJS1hX^vPXwnsQhqjM$*EK;<<-a-4%>$-3gtrBsIYUC$;hnnDhBW?AdZ-s zv6dP`ak!wW=(AgmfHRGNT#<70BuKRt2eD8IHlno(4E$6`nrAYwuMA5D0%vbRQc3}B zfwa~c0j8iOqk-9otN|%3sK5U6SyxuInN zD$3ZNJjCTU#Pj)mkn6or&}Sd97Z9bNW?7Ny$lQ%-sLsb?FE z#sw&X(-kXPwA&YFC=?3lKKR7T;{oBIchq1zgbLL*S@5e*RF1*`osUvm$b|Dk=^%yl zkTKMQXKK!(>WjX{~sK|0tZG)@7;1V*E=(IWp*!=MTVp-P{u(@87hQD?2N=pjSU8PbZvA%aU5$3UnV zYBy| zpbZKGwgSpyw8T1a$UrzC9Uc)x)Cxj`>S-ejPDV*Qw6-iIfQlMI;K5!{-!YU*Xih*) zvyK{tYHWN4<~W6+%tL7*VhlkL3x#&sp`2I*fCU(AHANNB7Nr(qL7^6aE+?6x$k5|N zy>Jc>Ys^{VB<>}S8KQ%7xl&dI^x6nZf@8qXc!C6gd|H&<01-4$Ln@Qh!X=#Mp=lDL zoF$HE+&T}9!Y;|Ikw(ILTdJ>1P)JlrU!HmEt0GVls0dU9DgqUOiai2vh_r z0u_OZKt-S;P!XsIR0Jvl6@iLCMW7;35vT}M1S$d*fr>yypdwHas0dU9DgqUOiai2vh_r0u_OZKt-S;P!XsIR0Jvl6@iLCMW7;35vT}M1S$d*fr>yypdwHas0dU9 zDgqUOiai2vh_r0u_OZKt-S;P!XsIR0Jvl6@iLCMW7;35vT}M1S$d*fr>yy zpdwHas0dU9Dgyss2pp_zq7Pxtoc2yn-*|J}rG+Rfm)#rF2X{8ug<%;3Sqcg=t4mi6^p?m=o_ zOP3$6Eoh6sEM2UmI7+uiZl3xtrB&+2!L_$0u*DuvE?`$XIh@SLtKS=wSuAz!GTTq_ zjvM;Vp5h(%Fb^@Xcintzu;O96w5(kj<(^Du4_14yW!1fi`|UgaeVF04w_P_IKWci{ zW^4mSfiZ6BS=QLljgfTkpL?|Ox9*!QJ;BYv!P@)#`RDNZ+^$>qmWlLrtLPo4=tY1l zo9V+ao0fQbBjXFK<6_=$EGu2Yx4QQU9-X(t!<+pMA^fw9w~OYb-txCM{6^0LEpiNZ zMvHd+{`Q`~a0M>j-b*Lz4We7d+Py_~Z)<(Nh^_CivdZ2-^b8~L;RT-dGCU}0qWm0Dc?l>6wB@mH?|$i?)LUHZVa}c!m)qNhP$}4 z_r~8h-yHi{x|1^;(Hp%-UTGeHS9zRbb2SGInvza%aBt`y-{WMWR zm+fHVyxSKM_P0x%Th`tiAMf7AdmpZs3`VnX$b`k5E!J-tu8zz|SigFt;lVx}Yb~tU xU;zjlJ-AXjUL0_txfx!5)QE%gEu-*-bvPB+jr3sye{ST%#;=<*p~TX$`=3_O$(#TH diff --git a/tests/php/fixtures/small_valid.pdf b/tests/php/fixtures/small_valid.pdf deleted file mode 100644 index 57bdf3d941c9f10701252ebc5284681dc52f7370..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1163 zcmah|zi-n(6s|yGxPgrV62nQTYC&r5&i2`7s469PT*?nMav}vtJGjoZ4WxEtUr@@9 zgqWES5*32IfIti^C=84pK>Ps+G4N0DE=|*r4p=(h`Fro)``-7SXDWU$&k8o0>A&iK z>VN3JKnA8bUB8DG7RXJRY^p760uqv-)mAbiK|4~?mvP#Zi;E~%nT)nk@42~e*5-a+ zuF(qJH|g67eQ$j?4OZWy?AtpEw(GypkMQeHqc-*Vr7E0!{PfZ5QsL~A_1$ZySFe8A zyYTT1o?bmuKUIG=UEe)1b@9*nb4Vu5VFEa5PDoSH2VraD0OBLQ2nZdb_6Q|GnkcNj zLPRQA8ds%i5U88fWKXS)Ut&j>YC9cCD$%BXWfB5dIsge}o_4cX=2#~P$t~GzMWwXY zpzz6EUN9WaUCKFQf?;8?f)K7_n~vjhL#&adEbVqc*(#}JQIdCb?0AQGRpQGBtyqRj zC9;fF)&+5i2gD(Dj&)1IvEnqBVT(pQb2Lc~9kB!d0X@wKuOV`y)yx}MmmBI&FYQ8w zY%GH_k->{p)J``CisT1cAhObtNikNfG-(`!BDe3{#Ywl_K6uf8#1%&&Js295W(z?% z2q?8IN*#kzo5AiZ4w}^L#&Tr$z~;gNpMf8TJ8;johfCW`Sb&2IH67T?uv3UNNCgai znA$o2ia`xV4Z~!N3hK^NbC%NCOJjxiGP!{$b`b;`KG^2mIWwyj57Ba z#vB-u2|vpO9VRg5!gNkx27?w%VB9<&&mPaKvZ&RTnf`UFt%nl3BurCn`ZCj~jS_2R^9aGp8&}S}eP+zfcN-JmE*3>l nq{Wg+g|JJ$E4by^1`}?{uq{jfaJh;yHIUj79L>y>R|51GUEVg- From f18316475244e4279e37b7e8d2a0ef9d2a37c52b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:53:02 -0300 Subject: [PATCH 08/13] chore: update dependencies and handler Update composer.json and Pkcs12Handler changes from refactoring. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- composer.json | 4 ++- lib/Handler/SignEngine/Pkcs12Handler.php | 41 ++++++++++++------------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index 31daf4dd3c..be06320b0d 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,9 @@ }, "autoload-dev": { "psr-4": { - "OCP\\": "vendor/nextcloud/ocp/OCP" + "OCP\\": "vendor/nextcloud/ocp/OCP", + "OCA\\Libresign\\Tests\\Unit\\": "tests/php/Unit/", + "OCA\\Libresign\\Tests\\Fixtures\\": "tests/php/fixtures/" } }, "require": { diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 11c1a3aaf3..d5040814ec 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -52,32 +52,23 @@ public function __construct( private function getSignatures($resource): iterable { rewind($resource); $content = stream_get_contents($resource); - preg_match_all( - '/ByteRange\s*\[\s*(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s*\]/', - $content, - $bytes - ); - if (empty($bytes['offset1']) || empty($bytes['length1']) || empty($bytes['offset2']) || empty($bytes['length2'])) { - throw new LibresignException($this->l10n->t('Unsigned file.')); - } - for ($i = 0; $i < count($bytes['offset1']); $i++) { - $offset1 = (int)$bytes['offset1'][$i]; - $length1 = (int)$bytes['length1'][$i]; - $offset2 = (int)$bytes['offset2'][$i]; + preg_match_all('/\/Contents\s*<([0-9a-fA-F]+)>/', $content, $contents, PREG_OFFSET_CAPTURE); - $signatureStart = $offset1 + $length1 + 1; - $signatureLength = $offset2 - $signatureStart - 1; + if (empty($contents[1])) { + throw new LibresignException($this->l10n->t('Unsigned file.')); + } - rewind($resource); + $seenHexSignatures = []; + foreach ($contents[1] as $match) { + $signatureHex = $match[0]; - $signature = stream_get_contents($resource, $signatureLength, $signatureStart); - if ($signature === false) { - yield null; + if (isset($seenHexSignatures[$signatureHex])) { continue; } + $seenHexSignatures[$signatureHex] = true; - $decodedSignature = @hex2bin($signature); + $decodedSignature = @hex2bin($signatureHex); if ($decodedSignature === false) { yield null; continue; @@ -101,7 +92,17 @@ public function getCertificateChain($resource): array { $certificates = []; foreach ($this->getSignatures($resource) as $signature) { - $certificates[] = $this->processSignature($resource, $signature); + if (!$signature) { + continue; + } + + $result = $this->processSignature($resource, $signature); + + if (empty($result['chain'])) { + continue; + } + + $certificates[] = $result; } return $certificates; } From 42caf29a6d5b1880af30131784461d6306908767 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:56:06 -0300 Subject: [PATCH 09/13] chore: add REUSE headers to catalog.yaml Add SPDX license and copyright headers to comply with REUSE specification. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/fixtures/pdfs/catalog.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/php/fixtures/pdfs/catalog.yaml b/tests/php/fixtures/pdfs/catalog.yaml index 44b4c6199c..df26014332 100644 --- a/tests/php/fixtures/pdfs/catalog.yaml +++ b/tests/php/fixtures/pdfs/catalog.yaml @@ -1,5 +1,8 @@ +# SPDX-FileCopyrightText: 2025 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# # LibreSign PDF Test Fixtures Catalog -# +# # This file describes all PDF test fixtures used in the test suite. # Each entry provides detailed metadata about the PDF signature characteristics. # From f3676ac9e195710721e38bc6cce1878c1ba923b7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:56:28 -0300 Subject: [PATCH 10/13] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/SignFileServiceTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index e272fb9a94..ef6a4b0801 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -36,7 +36,6 @@ use OCA\Libresign\Service\PdfSignatureDetectionService; use OCA\Libresign\Service\SignerElementsService; use OCA\Libresign\Service\SignFileService; -use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; From f392dad6ef9e90d055164019fe68dfec33812726 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:56:34 -0300 Subject: [PATCH 11/13] fix: year Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/fixtures/PdfFixtureCatalog.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/fixtures/PdfFixtureCatalog.php b/tests/php/fixtures/PdfFixtureCatalog.php index 9a33fa3c80..02467c9730 100644 --- a/tests/php/fixtures/PdfFixtureCatalog.php +++ b/tests/php/fixtures/PdfFixtureCatalog.php @@ -2,7 +2,7 @@ declare(strict_types=1); /** - * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors + * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ From 1841bd844f7bc7f28b9164f0a9c3fb4e60cd308c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 16:02:35 -0300 Subject: [PATCH 12/13] fix: update PDF path in DevelopController Update path from tests/php/fixtures/small_valid.pdf to tests/php/fixtures/pdfs/small_valid.pdf after file reorganization. Fixes /apps/libresign/develop/pdf route. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/DevelopController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Controller/DevelopController.php b/lib/Controller/DevelopController.php index b42f6b9fca..b724144b8f 100644 --- a/lib/Controller/DevelopController.php +++ b/lib/Controller/DevelopController.php @@ -48,7 +48,7 @@ public function pdf(): FileDisplayResponse|Response { if (!$this->isDebugMode()) { return new DataResponse([], Http::STATUS_NOT_FOUND); } - $file = new InMemoryFile('file.pdf', file_get_contents(__DIR__ . '/../../tests/php/fixtures/small_valid.pdf')); + $file = new InMemoryFile('file.pdf', file_get_contents(__DIR__ . '/../../tests/php/fixtures/pdfs/small_valid.pdf')); $response = new FileDisplayResponse($file); $response->setHeaders([ 'Content-Disposition' => 'inline; filename="file.pdf"', From ff9c3876cf6c2d92b45cfaecb5d14740e1c20269 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 15 Dec 2025 16:06:04 -0300 Subject: [PATCH 13/13] chore: update PDF paths in REUSE.toml Update paths to reflect new organization in pdfs/ subdirectory: - tests/php/fixtures/real_jsignpdf_level1.pdf -> tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf - tests/php/fixtures/small_valid-signed.pdf -> tests/php/fixtures/pdfs/small_valid-signed.pdf - tests/php/fixtures/small_valid.pdf -> tests/php/fixtures/pdfs/small_valid.pdf Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- REUSE.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/REUSE.toml b/REUSE.toml index 4e7bea0c17..0b6ae4a1eb 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -50,9 +50,9 @@ path = [ "src/types/openapi/openapi.ts", "tests/php/Unit/Handler/mock/cert.json", "tests/php/fixtures/cfssl/newcert-with-success.json", - "tests/php/fixtures/real_jsignpdf_level1.pdf", - "tests/php/fixtures/small_valid-signed.pdf", - "tests/php/fixtures/small_valid.pdf", + "tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf", + "tests/php/fixtures/pdfs/small_valid-signed.pdf", + "tests/php/fixtures/pdfs/small_valid.pdf", "tests/integration/composer.json", "tests/integration/composer.lock", "tests/integration/features/**/*.feature",