diff --git a/pdfcompare.php b/bin/pdfcompare.php similarity index 50% rename from pdfcompare.php rename to bin/pdfcompare.php index 1aa9d72..42c9a89 100644 --- a/pdfcompare.php +++ b/bin/pdfcompare.php @@ -19,35 +19,33 @@ along with this program. If not, see . */ +use ddn\sapp\AlmostOriginalLogger; use ddn\sapp\PDFDoc; -use function ddn\sapp\helpers\p_debug_var; -use function ddn\sapp\helpers\p_debug; -use ddn\sapp\pdfvalue\PDFValueObject; - -require_once('vendor/autoload.php'); - -if ($argc !== 3) - fwrite(STDERR, sprintf("usage: %s ", $argv[0])); -else { - if (!file_exists($argv[1])) { - fwrite(STDERR, "failed to open file " . $argv[1]); - die(); - } - if (!file_exists($argv[2])) { - fwrite(STDERR, "failed to open file " . $argv[2]); - die(); - } - - $doc1 = PDFDoc::from_string(file_get_contents($argv[1])); - if ($doc1 === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - - $doc2 = PDFDoc::from_string(file_get_contents($argv[2])); - if ($doc2 === false) - fwrite(STDERR, "failed to parse file " . $argv[2]); - - $differences = $doc1->compare($doc2); - foreach ($differences as $oid => $obj) { - print($obj->to_pdf_entry()); - } + +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc !== 3) { + fwrite(STDERR, sprintf('usage: %s ', $argv[0])); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); + exit(1); +} + +if (! file_exists($argv[2])) { + fwrite(STDERR, 'failed to open file ' . $argv[2]); + exit(1); +} + +$doc1 = PDFDoc::from_string(file_get_contents($argv[1])); +$doc1->setLogger(new AlmostOriginalLogger()); + +$doc2 = PDFDoc::from_string(file_get_contents($argv[2])); +$doc2->setLogger(new AlmostOriginalLogger()); + +$differences = $doc1->compare($doc2); +foreach ($differences as $obj) { + print $obj->to_pdf_entry(); } diff --git a/bin/pdfdeflate.php b/bin/pdfdeflate.php new file mode 100644 index 0000000..ab6f23e --- /dev/null +++ b/bin/pdfdeflate.php @@ -0,0 +1,82 @@ +. +*/ + +use ddn\sapp\AlmostOriginalLogger; +use ddn\sapp\PDFDoc; + +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc < 2 || $argc > 3) { + fwrite(STDERR, sprintf('usage: %s [oid]', $argv[0])); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); + exit(1); +} + +$doc = PDFDoc::from_string(file_get_contents($argv[1])); +$doc->setLogger(new AlmostOriginalLogger()); + +$toid = null; +if ($argc === 3) { + $toid = (int) $argv[2]; +} + +foreach ($doc->get_object_iterator() as $oid => $object) { + if ($toid !== null && $oid !== $toid) { + continue; + } + + if ($object === false) { + continue; + } + + if ($object['Filter'] === '/FlateDecode' && $object['Subtype'] !== '/Image') { + $stream = $object->get_stream(false); + if ($stream !== false) { + unset($object['Filter']); + $object->set_stream($stream, false); + $doc->add_object($object); + } + } + + // Not needed because we are rebuilding the document + if ($object['Type'] === '/ObjStm') { + $object->set_stream('', false); + $doc->add_object($object); + } + + // Do not want images to be uncompressed + if ($object['Subtype'] === '/Image') { + $object->set_stream(''); + $doc->add_object($object); + } + + if ($toid !== null) { + print $object->get_stream(false); + } +} + +if ($toid === null) { + echo $doc->to_pdf_file_s(true); +} diff --git a/pdfrebuild.php b/bin/pdfrebuild.php similarity index 60% rename from pdfrebuild.php rename to bin/pdfrebuild.php index 609828f..a5eb57d 100644 --- a/pdfrebuild.php +++ b/bin/pdfrebuild.php @@ -19,25 +19,25 @@ along with this program. If not, see . */ +use ddn\sapp\AlmostOriginalLogger; use ddn\sapp\PDFDoc; -require_once('vendor/autoload.php'); - -if (($argc < 2) || ($argc > 3)) - fwrite(STDERR, sprintf("usage: %s []", $argv[0])); -else { - if (!file_exists($argv[1])) - fwrite(STDERR, "failed to open file " . $argv[1]); - else { - $obj = PDFDoc::from_string(file_get_contents($argv[1])); - - if ($obj === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - else { - if ($argc == 3) - file_put_contents($argv[2], $obj->to_pdf_file_s(true)); - else - echo $obj->to_pdf_file_s(true); - } +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc < 2 || $argc > 3) { + fwrite(STDERR, sprintf('usage: %s []', $argv[0])); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); +} else { + $obj = PDFDoc::from_string(file_get_contents($argv[1])); + $obj->setLogger(new AlmostOriginalLogger()); + + if ($argc === 3) { + file_put_contents($argv[2], $obj->to_pdf_file_s(true)); + } else { + echo $obj->to_pdf_file_s(true); } } diff --git a/bin/pdfsign.php b/bin/pdfsign.php new file mode 100644 index 0000000..c59dfaf --- /dev/null +++ b/bin/pdfsign.php @@ -0,0 +1,56 @@ +. +*/ + +use ddn\sapp\AlmostOriginalLogger; +use ddn\sapp\PDFDoc; + +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc !== 3) { + fwrite(STDERR, sprintf('usage: %s ', $argv[0])); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); +} else { + // Silently prompt for the password + fwrite(STDERR, 'Password: '); + system('stty -echo'); + $password = trim(fgets(STDIN)); + system('stty echo'); + fwrite(STDERR, "\n"); + + $file_content = file_get_contents($argv[1]); + $obj = PDFDoc::from_string($file_content); + $obj->setLogger(new AlmostOriginalLogger()); + + if (! $obj->set_signature_certificate($argv[2], $password)) { + fwrite(STDERR, 'the certificate is not valid'); + } else { + $docsigned = $obj->to_pdf_file_s(); + if ($docsigned === false) { + fwrite(STDERR, 'could not sign the document'); + } else { + echo $docsigned; + } + } +} diff --git a/bin/pdfsigni.php b/bin/pdfsigni.php new file mode 100644 index 0000000..fcc40c5 --- /dev/null +++ b/bin/pdfsigni.php @@ -0,0 +1,93 @@ +#!/usr/bin/env php +. +*/ + +use ddn\sapp\AlmostOriginalLogger; +use ddn\sapp\PDFDoc; +use ddn\sapp\PDFException; + +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc !== 4) { + fwrite(STDERR, sprintf('usage: %s ', $argv[0])); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); + exit(1); +} + +// Silently prompt for the password +fwrite(STDERR, 'Password: '); +system('stty -echo'); +$password = trim(fgets(STDIN)); +system('stty echo'); +fwrite(STDERR, "\n"); + +$file_content = file_get_contents($argv[1]); +$obj = PDFDoc::from_string($file_content); +$obj->setLogger(new AlmostOriginalLogger()); + +$position = []; +$image = $argv[2]; +$imagesize = getimagesize($image); +if ($imagesize === false) { + fwrite(STDERR, 'failed to open the image ' . $image); + + exit(1); +} + +$pagesize = $obj->get_page_size(0); +if ($pagesize === false) { + return throw new PDFException('failed to get page size'); +} + +$pagesize = explode(' ', $pagesize[0]->val()); +// Calculate the position of the image according to its size and the size of the page; +// the idea is to keep the aspect ratio and center the image in the page with a size +// of 1/3 of the size of the page. +$p_x = (int) ('' . $pagesize[0]); +$p_y = (int) ('' . $pagesize[1]); +$p_w = (int) ('' . $pagesize[2]) - $p_x; +$p_h = (int) ('' . $pagesize[3]) - $p_y; +[$i_w, $i_h] = $imagesize; + +$ratio_x = $p_w / $i_w; +$ratio_y = $p_h / $i_h; +$ratio = min($ratio_x, $ratio_y); + +$i_w = $i_w * $ratio / 3; +$i_h = $i_h * $ratio / 3; +$p_x = $p_w / 3; +$p_y = $p_h / 3; +// Set the image appearance and the certificate file +$obj->set_signature_appearance(0, [$p_x, $p_y, $p_x + $i_w, $p_y + $i_h], $image); +if (! $obj->set_signature_certificate($argv[3], $password)) { + fwrite(STDERR, 'the certificate is not valid'); +} else { + $docsigned = $obj->to_pdf_file_s(); + if ($docsigned == false) { + fwrite(STDERR, 'could not sign the document'); + } else { + echo $docsigned; + } +} diff --git a/bin/pdfsignlts.php b/bin/pdfsignlts.php new file mode 100644 index 0000000..3143573 --- /dev/null +++ b/bin/pdfsignlts.php @@ -0,0 +1,82 @@ +#!/usr/bin/env php +. +*/ + +use ddn\sapp\AlmostOriginalLogger; +use ddn\sapp\PDFDoc; + +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc < 3) { + fwrite( + STDERR, + sprintf( + "usage: %s \n +tsaUrl - optional TSA server url to timestamp pdf document. +", + $argv[0] + ) + ); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); + exit(1); +} + +// Silently prompt for the password +fwrite(STDERR, 'Password: '); +system('stty -echo'); +$password = trim(fgets(STDIN)); +system('stty echo'); +fwrite(STDERR, "\n"); + +$tsa = $argv[3] ?? null; +if ($tsa === null || $tsa === '' || $tsa === '0') { + // Silently prompt for the timestamp autority + fwrite(STDERR, 'TSA("http://timestamp.digicert.com") type "no" to bypass tsa: '); + system('stty -echo'); + $tsa = trim(fgets(STDIN)) ?: 'http://timestamp.digicert.com'; + system('stty echo'); + fwrite(STDERR, "\n"); +} + +$file_content = file_get_contents($argv[1]); +$obj = PDFDoc::from_string($file_content); +$obj->setLogger(new AlmostOriginalLogger()); + +if (! $obj->set_signature_certificate($argv[2], $password)) { + fwrite(STDERR, 'the certificate is not valid'); + exit(1); +} + +if ($tsa !== 'no') { + $obj->set_tsa($tsa); +} + +$obj->set_ltv(); +$docsigned = $obj->to_pdf_file_s(); +if ($docsigned == false) { + fwrite(STDERR, 'could not sign the document'); +} else { + echo $docsigned; +} diff --git a/bin/pdfsigntsa.php b/bin/pdfsigntsa.php new file mode 100644 index 0000000..01fff9b --- /dev/null +++ b/bin/pdfsigntsa.php @@ -0,0 +1,73 @@ +#!/usr/bin/env php +. +*/ + +use ddn\sapp\AlmostOriginalLogger; +use ddn\sapp\PDFDoc; + +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc < 3) { + fwrite( + STDERR, + sprintf( + "usage: %s \n +tsaUrl - optional TSA server url to timestamp pdf document. +", + $argv[0] + ) + ); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); + exit(1); +} + +// Silently prompt for the password +fwrite(STDERR, 'Password: '); +system('stty -echo'); +$password = trim(fgets(STDIN)); +system('stty echo'); +fwrite(STDERR, "\n"); + +$tsa = $argv[3] ?? null; +if ($tsa === null || $tsa === '' || $tsa === '0') { + // Silently prompt for the timestamp autority + fwrite(STDERR, 'TSA("http://timestamp.digicert.com"): '); + system('stty -echo'); + $tsa = trim(fgets(STDIN)) ?: 'http://timestamp.digicert.com'; + system('stty echo'); + fwrite(STDERR, "\n"); +} + +$file_content = file_get_contents($argv[1]); +$obj = PDFDoc::from_string($file_content); +$obj->setLogger(new AlmostOriginalLogger()); + +if (! $obj->set_signature_certificate($argv[2], $password)) { + fwrite(STDERR, 'the certificate is not valid'); +} else { + $obj->set_tsa($tsa); + $docsigned = $obj->to_pdf_file_s(); + if ($docsigned == false) { + fwrite(STDERR, 'could not sign the document'); + } else { + echo $docsigned; + } +} diff --git a/bin/pdfsignx.php b/bin/pdfsignx.php new file mode 100644 index 0000000..59679cd --- /dev/null +++ b/bin/pdfsignx.php @@ -0,0 +1,59 @@ +#!/usr/bin/env php +. +*/ + +use ddn\sapp\AlmostOriginalLogger; +use ddn\sapp\PDFDoc; + +require_once __DIR__ . '/../vendor/autoload.php'; + +if ($argc !== 4) { + fwrite(STDERR, sprintf('usage: %s ', $argv[0])); + exit(1); +} + +if (! file_exists($argv[1])) { + fwrite(STDERR, 'failed to open file ' . $argv[1]); + exit(1); +} + +// Silently prompt for the password +fwrite(STDERR, 'Password: '); +system('stty -echo'); +$password = trim(fgets(STDIN)); +system('stty echo'); +fwrite(STDERR, "\n"); + +$file_content = file_get_contents($argv[1]); +$obj = PDFDoc::from_string($file_content); +$obj->setLogger(new AlmostOriginalLogger()); + +$signedDoc = $obj->sign_document($argv[3], $password, 0, $argv[2]); +if ($signedDoc === false) { + fwrite(STDERR, 'failed to sign the document'); +} else { + $docsigned = $signedDoc->to_pdf_file_s(); + if ($docsigned == false) { + fwrite(STDERR, 'could not sign the document'); + } else { + echo $docsigned; + } +} diff --git a/stty.bat b/bin/stty.bat similarity index 100% rename from stty.bat rename to bin/stty.bat diff --git a/composer.json b/composer.json index bbe9288..503d6bf 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,8 @@ "keywords": ["php", "pdf", "signature", "sign", "electronic signature"], "type": "library", "license": "LGPL-3.0-or-later", + "minimum-stability": "stable", + "prefer-stable": true, "authors": [ { "name": "Carlos A.", @@ -11,11 +13,32 @@ } ], "require": { - "php": ">=7.4" + "php": ">=8.1", + "ext-curl": "*", + "ext-fileinfo": "*", + "ext-openssl": "*", + "ext-zlib": "*", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "lendable/composer-license-checker": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "rector/rector": "*", + "roave/security-advisories": "dev-latest", + "slevomat/coding-standard": "^8.0", + "symplify/easy-coding-standard": "^12.1", + "symplify/phpstan-rules": "^12" }, "autoload": { "psr-4": { "ddn\\sapp\\": "src/"} }, - "minimum-stability": "dev" + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } + } } diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..a981a56 --- /dev/null +++ b/ecs.php @@ -0,0 +1,33 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/bin', + ]) + + // add a single rule + ->withRules([ + NoUnusedImportsFixer::class, + ]) + ->withPreparedSets( + arrays: true, + controlStructures: true, + psr12: true, + comments: true, + docblocks: true, + spaces: true, + cleanCode: true, + namespaces: true, + ) + ->withSkip([ + RemoveUselessDefaultCommentFixer::class, + ParamReturnAndVarTagMalformsFixer::class, + ]); diff --git a/pdfdeflate.php b/pdfdeflate.php deleted file mode 100644 index e8d395f..0000000 --- a/pdfdeflate.php +++ /dev/null @@ -1,78 +0,0 @@ -. -*/ - -use ddn\sapp\PDFDoc; - -require_once('vendor/autoload.php'); - -if (($argc < 2) or ($argc > 3)) - fwrite(STDERR, sprintf("usage: %s [oid]", $argv[0])); -else { - if (!file_exists($argv[1])) - fwrite(STDERR, "failed to open file " . $argv[1]); - else { - $doc = PDFDoc::from_string(file_get_contents($argv[1])); - - $toid = null; - if ($argc === 3) - $toid = intval($argv[2]); - - if ($doc === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - else { - foreach ($doc->get_object_iterator() as $oid => $object) { - if ($toid !== null) { - if ($oid != $toid) { - continue; - } - } - - if ($object === false) - continue; - if ($object["Filter"] == "/FlateDecode") { - if ($object["Subtype"] != "/Image") { - $stream = $object->get_stream(false); - if ($stream !== false) { - unset($object["Filter"]); - $object->set_stream($stream, false); - $doc->add_object($object); - } - } - } - // Not needed because we are rebuilding the document - if ($object["Type"] == "/ObjStm") { - $object->set_stream("", false); - $doc->add_object($object); - } - // Do not want images to be uncompressed - if ($object["Subtype"] == "/Image") { - $object->set_stream(""); - $doc->add_object($object); - } - if ($toid != null) { - print($object->get_stream(false)); - } - } - if ($toid === null) - echo $doc->to_pdf_file_s(true); - } - } -} diff --git a/pdfsign.php b/pdfsign.php deleted file mode 100644 index ed7db05..0000000 --- a/pdfsign.php +++ /dev/null @@ -1,56 +0,0 @@ -. -*/ - -use ddn\sapp\PDFDoc; - -require_once('vendor/autoload.php'); - -if ($argc !== 3) - fwrite(STDERR, sprintf("usage: %s ", $argv[0])); -else { - if (!file_exists($argv[1])) - fwrite(STDERR, "failed to open file " . $argv[1]); - else { - // Silently prompt for the password - fwrite(STDERR, "Password: "); - system('stty -echo'); - $password = trim(fgets(STDIN)); - system('stty echo'); - fwrite(STDERR, "\n"); - - $file_content = file_get_contents($argv[1]); - $obj = PDFDoc::from_string($file_content); - - if ($obj === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - else { - if (!$obj->set_signature_certificate($argv[2], $password)) { - fwrite(STDERR, "the certificate is not valid"); - } else { - $docsigned = $obj->to_pdf_file_s(); - if ($docsigned === false) - fwrite(STDERR, "could not sign the document"); - else - echo $docsigned; - } - } - } -} diff --git a/pdfsigni.php b/pdfsigni.php deleted file mode 100755 index cd8bcd4..0000000 --- a/pdfsigni.php +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env php -. -*/ - -use ddn\sapp\PDFDoc; - -require_once('vendor/autoload.php'); - -if ($argc !== 4) - fwrite(STDERR, sprintf("usage: %s ", $argv[0])); -else { - if (!file_exists($argv[1])) - fwrite(STDERR, "failed to open file " . $argv[1]); - else { - // Silently prompt for the password - fwrite(STDERR, "Password: "); - system('stty -echo'); - $password = trim(fgets(STDIN)); - system('stty echo'); - fwrite(STDERR, "\n"); - - $file_content = file_get_contents($argv[1]); - $obj = PDFDoc::from_string($file_content); - - if ($obj === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - else { - $position = [ ]; - $image = $argv[2]; - $imagesize = @getimagesize($image); - if ($imagesize === false) { - fwrite(STDERR, "failed to open the image $image"); - return; - } - $pagesize = $obj->get_page_size(0); - if ($pagesize === false) - return p_error("failed to get page size"); - - $pagesize = explode(" ", $pagesize[0]->val()); - // Calculate the position of the image according to its size and the size of the page; - // the idea is to keep the aspect ratio and center the image in the page with a size - // of 1/3 of the size of the page. - $p_x = intval("". $pagesize[0]); - $p_y = intval("". $pagesize[1]); - $p_w = intval("". $pagesize[2]) - $p_x; - $p_h = intval("". $pagesize[3]) - $p_y; - $i_w = $imagesize[0]; - $i_h = $imagesize[1]; - - $ratio_x = $p_w / $i_w; - $ratio_y = $p_h / $i_h; - $ratio = min($ratio_x, $ratio_y); - - $i_w = ($i_w * $ratio) / 3; - $i_h = ($i_h * $ratio) / 3; - $p_x = $p_w / 3; - $p_y = $p_h / 3; - - // Set the image appearance and the certificate file - $obj->set_signature_appearance(0, [ $p_x, $p_y, $p_x + $i_w, $p_y + $i_h ], $image); - if (!$obj->set_signature_certificate($argv[3], $password)) { - fwrite(STDERR, "the certificate is not valid"); - } else { - $docsigned = $obj->to_pdf_file_s(); - if ($docsigned === false) - fwrite(STDERR, "could not sign the document"); - else - echo $docsigned; - } - } - } -} diff --git a/pdfsignlts.php b/pdfsignlts.php deleted file mode 100644 index f5e9692..0000000 --- a/pdfsignlts.php +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env php -. -*/ - -use ddn\sapp\PDFDoc; - -require_once('vendor/autoload.php'); - -if ($argc < 3) - fwrite(STDERR, sprintf("usage: %s \n -tsaUrl - optional TSA server url to timestamp pdf document. -", $argv[0])); -else { - if (!file_exists($argv[1])) - fwrite(STDERR, "failed to open file " . $argv[1]); - else { - // Silently prompt for the password - fwrite(STDERR, "Password: "); - system('stty -echo'); - $password = trim(fgets(STDIN)); - system('stty echo'); - fwrite(STDERR, "\n"); - - $tsa = $argv[3] ?? null; - if (empty($tsa)) { - // Silently prompt for the timestamp autority - fwrite(STDERR, "TSA(\"http://timestamp.digicert.com\") type \"no\" to bypass tsa: "); - system('stty -echo'); - $tsa = trim(fgets(STDIN)) ?: "http://timestamp.digicert.com"; - system('stty echo'); - fwrite(STDERR, "\n"); - } - - $file_content = file_get_contents($argv[1]); - $obj = PDFDoc::from_string($file_content); - - if ($obj === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - else { - if (!$obj->set_signature_certificate($argv[2], $password)) - fwrite(STDERR, "the certificate is not valid"); - else { - if ($tsa != 'no') { - $obj->set_tsa($tsa); - } - $obj->set_ltv(); - $docsigned = $obj->to_pdf_file_s(); - if ($docsigned === false) - fwrite(STDERR, "could not sign the document"); - else - echo $docsigned; - } - } - } -} diff --git a/pdfsigntsa.php b/pdfsigntsa.php deleted file mode 100644 index e4e848c..0000000 --- a/pdfsigntsa.php +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env php -. -*/ - -use ddn\sapp\PDFDoc; - -require_once('vendor/autoload.php'); - -if ($argc < 3) - fwrite(STDERR, sprintf("usage: %s \n -tsaUrl - optional TSA server url to timestamp pdf document. -", $argv[0])); -else { - if (!file_exists($argv[1])) - fwrite(STDERR, "failed to open file " . $argv[1]); - else { - // Silently prompt for the password - fwrite(STDERR, "Password: "); - system('stty -echo'); - $password = trim(fgets(STDIN)); - system('stty echo'); - fwrite(STDERR, "\n"); - - $tsa = $argv[3] ?? null; - if (empty($tsa)) { - // Silently prompt for the timestamp autority - fwrite(STDERR, "TSA(\"http://timestamp.digicert.com\"): "); - system('stty -echo'); - $tsa = trim(fgets(STDIN)) ?: "http://timestamp.digicert.com"; - system('stty echo'); - fwrite(STDERR, "\n"); - } - - $file_content = file_get_contents($argv[1]); - $obj = PDFDoc::from_string($file_content); - - if ($obj === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - else { - if (!$obj->set_signature_certificate($argv[2], $password)) - fwrite(STDERR, "the certificate is not valid"); - else { - $obj->set_tsa($tsa); - $docsigned = $obj->to_pdf_file_s(); - if ($docsigned === false) - fwrite(STDERR, "could not sign the document"); - else - echo $docsigned; - } - } - } -} diff --git a/pdfsignx.php b/pdfsignx.php deleted file mode 100755 index 0150e3b..0000000 --- a/pdfsignx.php +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env php -. -*/ - -use ddn\sapp\PDFDoc; - -require_once('vendor/autoload.php'); - -if ($argc !== 4) - fwrite(STDERR, sprintf("usage: %s ", $argv[0])); -else { - if (!file_exists($argv[1])) - fwrite(STDERR, "failed to open file " . $argv[1]); - else { - // Silently prompt for the password - fwrite(STDERR, "Password: "); - system('stty -echo'); - $password = trim(fgets(STDIN)); - system('stty echo'); - fwrite(STDERR, "\n"); - - $file_content = file_get_contents($argv[1]); - $obj = PDFDoc::from_string($file_content); - - if ($obj === false) - fwrite(STDERR, "failed to parse file " . $argv[1]); - else { - $signedDoc = $obj->sign_document($argv[3], $password, 0, $argv[2]); - if ($signedDoc === false) { - fwrite(STDERR, "failed to sign the document"); - } else { - $docsigned = $signedDoc->to_pdf_file_s(); - if ($docsigned === false) - fwrite(STDERR, "could not sign the document"); - else - echo $docsigned; - } - } - } -} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..0416964 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,21 @@ +parameters: + level: 2 + paths: + - src/ + - bin/ + parallel: + jobSize: 2 + + reportUnmatchedIgnoredErrors: true + inferPrivatePropertyTypeFromConstructor: true + checkMissingCallableSignature: false + treatPhpDocTypesAsCertain: false + + ignoreErrors: + - "#Implicit array creation is not allowed#" + - "#Call to an undefined static method .*asn1::#" + - "#Construct empty\\(\\) is not allowed. Use more strict comparison.#" +# - + # identifier: missingType.iterableValue + # - + # identifier: missingType.generics diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..d18757c --- /dev/null +++ b/rector.php @@ -0,0 +1,34 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/bin', + ]) + // uncomment to reach your current PHP version + ->withPhpSets(php80: true) + ->withImportNames() + ->withParallel(jobSize: 2) +// ->withTypeCoverageLevel(1) +// ->withDeadCodeLevel(1) +// ->withPreparedSets(deadCode: true, codeQuality: true, codingStyle: true, typeDeclarations: true) + ->withPreparedSets(typeDeclarations: true) + ->withPreparedSets(codeQuality: true) + ->withPreparedSets(codingStyle: true) + ->withPreparedSets(deadCode: true) + ->withSkip([ + RemoveDeadIfForeachForRector::class, + RemoveDeadLoopRector::class, + RemoveDuplicatedCaseInSwitchRector::class, + RemoveUnreachableStatementRector::class, + RemoveAlwaysTrueIfConditionRector::class, + ]); diff --git a/src/AlmostOriginalLogger.php b/src/AlmostOriginalLogger.php new file mode 100644 index 0000000..639ee4a --- /dev/null +++ b/src/AlmostOriginalLogger.php @@ -0,0 +1,17 @@ +_max_oid++; - return $this->_max_oid; + protected ?LoggerInterface $logger = null; + + public function setLogger(?LoggerInterface $logger = null): void + { + $this->logger = $logger; } /** * Retrieve the number of pages in the document (not considered those pages that could be added by the user using this object or derived ones) + * * @return pagecount number of pages in the original document */ - public function get_page_count() { + public function get_page_count(): int + { return count($this->_pages_info); } @@ -107,86 +118,104 @@ public function get_page_count() { * the state using function "pop_state". Many states can be stored, and they will be retrieved in reverse order * using pop_state */ - public function push_state() { + public function push_state(): void + { $cloned_objects = []; foreach ($this->_pdf_objects as $oid => $object) { $cloned_objects[$oid] = clone $object; } - array_push($this->_backup_state, [ 'max_oid' => $this->_max_oid, 'pdf_objects' => $cloned_objects ]); + + $this->_backup_state[] = [ + 'max_oid' => $this->_max_oid, + 'pdf_objects' => $cloned_objects, + ]; } /** * Function that retrieves an stored state by means of function "push_state" + * * @return restored true if a previous state was restored; false if there was no stored state */ - public function pop_state() { - if (count($this->_backup_state) > 0) { + public function pop_state(): bool + { + if ($this->_backup_state !== []) { $state = array_pop($this->_backup_state); $this->_max_oid = $state['max_oid']; $this->_pdf_objects = $state['pdf_objects']; + return true; } + return false; } /** * The function parses a document from a string: analyzes the structure and obtains and object * of type PDFDoc (if possible), or false, if an error happens. + * * @param buffer a string that contains the file to analyze * @param depth the number of previous versions to consider; if null, will consider any version; * otherwise only the object ids from the latest $depth versions will be considered * (if it is an incremental updated document) */ - public static function from_string($buffer, $depth = null) { + public static function from_string(string $buffer, ?int $depth = null): self + { $structure = PDFUtilFnc::acquire_structure($buffer, $depth); - if ($structure === false) - return false; + if ($structure === false) { + throw new PDFException('acquire_structure failed'); + } - $trailer = $structure["trailer"]; - $version = $structure["version"]; - $xref_table = $structure["xref"]; - $xref_position = $structure["xrefposition"]; - $revisions = $structure["revisions"]; + $trailer = $structure['trailer']; + $version = $structure['version']; + $xref_table = $structure['xref']; + $xref_position = $structure['xrefposition']; + $revisions = $structure['revisions']; - $pdfdoc = new PDFDoc(); + $pdfdoc = new self(); $pdfdoc->_pdf_version_string = $version; $pdfdoc->_pdf_trailer_object = $trailer; $pdfdoc->_xref_position = $xref_position; $pdfdoc->_xref_table = $xref_table; - $pdfdoc->_xref_table_version = $structure["xrefversion"]; + $pdfdoc->_xref_table_version = $structure['xrefversion']; $pdfdoc->_revisions = $revisions; $pdfdoc->_buffer = $buffer; - if ($trailer !== false) - if ($trailer['Encrypt'] !== false) - // TODO: include encryption (maybe borrowing some code: http://www.fpdf.org/en/script/script37.php) - p_error("encrypted documents are not fully supported; maybe you cannot get the expected results"); + if ($trailer !== false && $trailer['Encrypt'] !== false) { + // TODO: include encryption (maybe borrowing some code: http://www.fpdf.org/en/script/script37.php) + throw new PDFException('encrypted documents are not fully supported; maybe you cannot get the expected results'); + } $oids = array_keys($xref_table); sort($oids); $pdfdoc->_max_oid = array_pop($oids); - if ($trailer === false) - p_warning("invalid trailer object"); - else - $pdfdoc->_acquire_pages_info(); + if ($trailer === false) { + throw new PDFException('invalid trailer object'); + } + + $pdfdoc->_acquire_pages_info(); return $pdfdoc; } - public function get_revision($rev_i) { - if ($rev_i === null) + public function get_revision(?int $rev_i): string + { + if ($rev_i === null) { $rev_i = count($this->_revisions) - 1; - if ($rev_i < 0) + } + + if ($rev_i < 0) { $rev_i = count($this->_revisions) + $rev_i - 1; + } - return substr($this->_buffer, 0, $this->_revisions[$rev_i]); + return substr((string) $this->_buffer, 0, $this->_revisions[$rev_i]); } /** * Function that builds the object list from the xref table */ - public function build_objects_from_xref() { + public function build_objects_from_xref(): void + { foreach ($this->_xref_table as $oid => $obj) { $obj = $this->get_object($oid); $this->add_object($obj); @@ -198,21 +227,28 @@ public function build_objects_from_xref() { * This mechanism enables to walk over any object, either they are new ones or they were in the original doc. * Enables: * foreach ($doc->get_object_iterator() as $oid => obj) { ... } + * * @param allobjects the iterator obtains any possible object, according to the oids; otherwise, only will return the * objects that appear in the current version of the xref - * @return oid=>obj the objects + * + * @return Generator */ - public function get_object_iterator($allobjects = false) { - if ($allobjects === true) { + public function get_object_iterator(bool $allobjects = false) + { + if ($allobjects) { for ($i = 0; $i <= $this->_max_oid; $i++) { yield $i => $this->get_object($i); } } else { foreach ($this->_xref_table as $oid => $offset) { - if ($offset === null) continue; + if ($offset === null) { + continue; + } $o = $this->get_object($oid); - if ($o === false) continue; + if ($o === false) { + continue; + } yield $oid => $o; } @@ -222,40 +258,50 @@ public function get_object_iterator($allobjects = false) { /** * This function checks whether the passed object is a reference or not, and in case that * it is a reference, it returns the referenced object; otherwise it return the object itself + * * @param reference the reference value to obtain + * * @return obj it reference can be interpreted as a reference, the referenced object; otherwise, the object itself. * If the passed value is an array of references, it will return false */ - public function get_indirect_object( $reference ) { + public function get_indirect_object($reference) + { $object_id = $reference->get_object_referenced(); if ($object_id !== false) { - if (is_array($object_id)) + if (is_array($object_id)) { return false; + } + return $this->get_object($object_id); } + return $reference; } /** * Obtains an object from the document, usign the oid in the PDF document. + * * @param oid the oid of the object that is being retrieved * @param original if true and the object has been overwritten in this document, the object * retrieved will be the original one. Setting to false will retrieve the * more recent object + * * @return obj the object retrieved (or false if not found) */ - public function get_object($oid, $original_version = false) { - if ($original_version === true) { + public function get_object(int $oid, bool $original_version = false) + { + if ($original_version) { // Prioritizing the original version $object = PDFUtilFnc::find_object($this->_buffer, $this->_xref_table, $oid); - if ($object === false) - $object = $this->_pdf_objects[$oid]??false; - + if ($object === false) { + $object = $this->_pdf_objects[$oid] ?? false; + } } else { // Prioritizing the new versions - $object = $this->_pdf_objects[$oid]??false; - if ($object === false) + $object = $this->_pdf_objects[$oid] ?? false; + if ($object === false) { $object = PDFUtilFnc::find_object($this->_buffer, $this->_xref_table, $oid); + } } return $object; @@ -264,63 +310,77 @@ public function get_object($oid, $original_version = false) { /** * Function that sets the appearance of the signature (if the document is to be signed). At this time, it is possible to set * the page in which the signature will appear, the rectangle, and an image that will be shown in the signature form. + * * @param page the page (zero based) in which the signature will appear * @param rect the rectangle (in page-based coordinates) where the signature will appear in that page * @param imagefilename an image file name (or an image in a buffer, with symbol '@' prepended) that will be put inside the rect */ - public function set_signature_appearance($page_to_appear = 0, $rect_to_appear = [0, 0, 0, 0], $imagefilename = null) { + public function set_signature_appearance($page_to_appear = 0, $rect_to_appear = [0, 0, 0, 0], $imagefilename = null): void + { $this->_appearance = [ - "page" => $page_to_appear, - "rect" => $rect_to_appear, - "image" => $imagefilename + 'page' => $page_to_appear, + 'rect' => $rect_to_appear, + 'image' => $imagefilename, ]; } /** * Removes the settings of signature appearance (i.e. no signature will appear in the document) */ - public function clear_signature_appearance() { + public function clear_signature_appearance(): void + { $this->_appearance = null; } /** * Removes the certificate for the signature (i.e. the document will not be signed) */ - public function clear_signature_certificate() { + public function clear_signature_certificate(): void + { $this->_certificate = null; } /** * Function that stores the certificate to use, when signing the document + * * @param certfile a file that contains a user certificate in pkcs12 format, * or an array [ 'cert' => , 'pkey' => , 'extracerts' => ] * that would be the output of openssl_pkcs12_read * @param password the password to read the private key + * * @return valid true if the certificate can be used to sign the document, false otherwise */ - public function set_signature_certificate($certfile, $certpass = null) { + public function set_signature_certificate($certfile, ?string $certpass = null): bool + { // First we read the certificate if (is_array($certfile)) { $certificate = $certfile; - $certificate["pkey"] = [$certificate["pkey"], $certpass]; + $certificate['pkey'] = [$certificate['pkey'], $certpass]; // If a password is provided, we'll try to decode the private key - if (openssl_pkey_get_private($certificate["pkey"]) === false) - return p_error("invalid private key"); - if (! openssl_x509_check_private_key($certificate["cert"], $certificate["pkey"])) - return p_error("private key doesn't corresponds to certificate"); + if (openssl_pkey_get_private($certificate['pkey']) === false) { + throw new PDFException('invalid private key'); + } + + if (! openssl_x509_check_private_key($certificate['cert'], $certificate['pkey'])) { + throw new PDFException("private key doesn't corresponds to certificate"); + } if (is_string($certificate['extracerts'] ?? null)) { $certificate['extracerts'] = array_filter(explode("-----END CERTIFICATE-----\n", $certificate['extracerts'])); - foreach ($certificate['extracerts'] as &$extracerts) - $extracerts = $extracerts . "-----END CERTIFICATE-----\n"; + foreach ($certificate['extracerts'] as &$extracerts) { + $extracerts .= "-----END CERTIFICATE-----\n"; + } } } else { $certfilecontent = file_get_contents($certfile); - if ($certfilecontent === false) - return p_error("could not read file $certfile"); - if (openssl_pkcs12_read($certfilecontent, $certificate, $certpass) === false) - return p_error("could not get the certificates from file $certfile"); + if ($certfilecontent === false) { + throw new PDFException('could not read file ' . $certfile); + } + + if (openssl_pkcs12_read($certfilecontent, $certificate, $certpass) === false) { + throw new PDFException('could not get the certificates from file ' . $certfile); + } } // Store the certificate @@ -331,11 +391,13 @@ public function set_signature_certificate($certfile, $certpass = null) { /** * Function that stores the ltv configuration to use, when signing the document - * @param $ocspURI OCSP Url to validate cert file - * @param $crlURIorFILE Crl filename/url to validate cert - * @param $issuerURIorFILE issuer filename/url + * + * @param $ocspURI OCSP|null Url to validate cert file + * @param $crlURIorFILE Crl|null filename/url to validate cert + * @param $issuerURIorFILE issuer|null filename/url */ - public function set_ltv($ocspURI=null, $crlURIorFILE=null, $issuerURIorFILE=null) { + public function set_ltv(?string $ocspURI = null, ?string $crlURIorFILE = null, ?string $issuerURIorFILE = null): void + { $this->_signature_ltv_data['ocspURI'] = $ocspURI; $this->_signature_ltv_data['crlURIorFILE'] = $crlURIorFILE; $this->_signature_ltv_data['issuerURIorFILE'] = $issuerURIorFILE; @@ -343,11 +405,12 @@ public function set_ltv($ocspURI=null, $crlURIorFILE=null, $issuerURIorFILE=null /** * Function that stores the tsa configuration to use, when signing the document - * @param $tsaurl Link to tsa service - * @param $tsauser the user for tsa service - * @param $tsapass the password for tsa service + * + * @param $tsauser ?string user for tsa service + * @param $tsapass ?string password for tsa service */ - public function set_tsa($tsa, $tsauser = null, $tsapass = null) { + public function set_tsa(string $tsa, ?string $tsauser = null, ?string $tsapass = null): void + { $this->_signature_tsa['host'] = $tsa; if ($tsauser && $tsapass) { $this->_signature_tsa['user'] = $tsauser; @@ -357,13 +420,8 @@ public function set_tsa($tsa, $tsauser = null, $tsapass = null) { /** * Function to set the metadata properties for the certificate options - * @param $name - * @param $reason - * @param $location - * @param $contact - * @return void */ - public function set_metadata_props($name = null, $reason = null, $location = null, $contact = null) + public function set_metadata_props($name = null, $reason = null, $location = null, $contact = null): void { $this->_metadata_name = $name; $this->_metadata_reason = $reason; @@ -372,434 +430,144 @@ public function set_metadata_props($name = null, $reason = null, $location = nul } /** - * Function that creates and updates the PDF objects needed to sign the document. The workflow for a signature is: - * - create a signature object - * - create an annotation object whose value is the signature object - * - create a form object (along with other objects) that will hold the appearance of the annotation object - * - modify the root object to make acroform point to the annotation object - * - modify the page object to make the annotations of that page include the annotation object - * - * > If the appearance is not set, the image will not appear, and the signature object will be invisible. - * > If the certificate is not set, the signature created will be a placeholder (that acrobat will able to sign) + * Function that gets the objects that have been read from the document * - * LIMITATIONS: one document can be signed once at a time; if wanted more signatures, then chain the documents: - * $o1->set_signature_certificate(...); - * $o2 = PDFDoc::fromstring($o1->to_pdf_file_s); - * $o2->set_signature_certificate(...); - * $o2->to_pdf_file_s(); + * @return objects an array of objects, indexed by the oid of each object + */ + public function get_objects() + { + return $this->_pdf_objects; + } + + /** + * Function that gets the version of the document. It will have the form + * PDF-1.x * - * @return signature a signature object, or null if the document is not signed; false if an error happens + * @return version the PDF version */ - protected function _generate_signature_in_document() { - $imagefilename = null; - $recttoappear = [ 0, 0, 0, 0]; - $pagetoappear = 0; + public function get_version() + { + return $this->_pdf_version_string; + } - if ($this->_appearance !== null) { - $imagefilename = $this->_appearance["image"]; - $recttoappear = $this->_appearance["rect"]; - $pagetoappear = $this->_appearance["page"]; + /** + * Function that sets the version for the document. + * + * @param version the version of the PDF document (it shall have the form PDF-1.x) + * + * @return correct true if the version had the proper form; false otherwise + */ + public function set_version($version): bool + { + if (preg_match("/PDF-1.\[0-9\]/", (string) $version) !== 1) { + return false; } - // First of all, we are searching for the root object (which should be in the trailer) - $root = $this->_pdf_trailer_object["Root"]; - - if (($root === false) || (($root = $root->get_object_referenced()) === false)) - return p_error("could not find the root object from the trailer"); - - $root_obj = $this->get_object($root); - if ($root_obj === false) - return p_error("invalid root object"); + $this->_pdf_version_string = $version; - // Now the object corresponding to the page number in which to appear - $page_obj = $this->get_page($pagetoappear); - if ($page_obj === false) - return p_error("invalid page"); + return true; + } - // The objects to update - $updated_objects = [ ]; + /** + * Function that creates a new PDFObject and stores it in the document object list, so that + * it is automatically managed by the document. The returned object can be modified and + * that modifications will be reflected in the document. + * + * @param value the value that the object will contain + * + * @return obj the PDFObject created + */ + public function create_object($value = [], $class = PDFObject::class, $autoadd = true): PDFObject + { + $o = new $class($this->get_new_oid(), $value); + if ($autoadd === true) { + $this->add_object($o); + } - // Add the annotation to the page - if (!isset($page_obj["Annots"])) - $page_obj["Annots"] = new PDFValueList(); + return $o; + } - $annots = &$page_obj["Annots"]; - $page_rotation = $page_obj["Rotate"]??new PDFValueSimple(0); + /** + * Adds a pdf object to the document (overwrites the one with the same oid, if existed) + * + * @param pdf_object the object to add to the document + * + * @return true if the object was added; false otherwise (e.g. already exists an object of a greater generation) + */ + public function add_object(PDFObject $pdf_object): bool + { + $oid = $pdf_object->get_oid(); - if ((($referenced = $annots->get_object_referenced()) !== false) && (!is_array($referenced))) { - // It is an indirect object, so we need to update that object - $newannots = $this->create_object( - $this->get_object($referenced)->get_value() - ); - } else { - $newannots = $this->create_object( - new PDFValueList() - ); - $newannots->push($annots); + if (isset($this->_pdf_objects[$oid]) && $this->_pdf_objects[$oid]->get_generation() > $pdf_object->get_generation()) { + return false; } - // Create the annotation object, annotate the offset and append the object - $annotation_object = $this->create_object([ - "Type" => "/Annot", - "Subtype" => "/Widget", - "FT" => "/Sig", - "V" => new PDFValueString(""), - "T" => new PDFValueString('Signature' . get_random_string()), - "P" => new PDFValueReference($page_obj->get_oid()), - "Rect" => $recttoappear, - "F" => 132 // TODO: check this value - ] - ); + $this->_pdf_objects[$oid] = $pdf_object; - // Prepare the signature object (we need references to it) - $signature = null; - if ($this->_certificate !== null) { - // Perform signature test to get signature size to define __SIGNATURE_MAX_LENGTH - p_debug(" ########## PERFORM SIGNATURE LENGTH CHECK ##########\n"); - $CMS = new helpers\CMS; - $CMS->signature_data['signcert'] = $this->_certificate['cert']; - $CMS->signature_data['extracerts'] = $this->_certificate['extracerts']??null; - $CMS->signature_data['hashAlgorithm'] = 'sha256'; - $CMS->signature_data['privkey'] = $this->_certificate['pkey']; - $CMS->signature_data['tsa'] = $this->_signature_tsa; - $CMS->signature_data['ltv'] = $this->_signature_ltv_data; - $res = $CMS->pkcs7_sign('0'); - $len = strlen($res); - p_debug(" Signature Length is \"$len\" Bytes"); - p_debug(" ########## FINISHED SIGNATURE LENGTH CHECK #########\n\n"); - PDFSignatureObject::$__SIGNATURE_MAX_LENGTH = $len; + // Update the maximum oid + if ($oid > $this->_max_oid) { + $this->_max_oid = $oid; + } - $signature = $this->create_object([], PDFSignatureObject::class, false); - //$signature = new PDFSignatureObject([]); - $signature->set_metadata($this->_metadata_name, $this->_metadata_reason, $this->_metadata_location, $this->_metadata_contact_info); - $signature->set_certificate($this->_certificate); - if($this->_signature_tsa !== null) { - $signature->set_signature_tsa($this->_signature_tsa); - } - if($this->_signature_ltv_data !== null) { - $signature->set_signature_ltv($this->_signature_ltv_data); - } + return true; + } - // Update the value to the annotation object - $annotation_object["V"] = new PDFValueReference($signature->get_oid()); + /** + * This functions outputs the document to a buffer object, ready to be dumped to a file. + * + * @param rebuild whether we are rebuilding the whole xref table or not (in case of incremental versions, we should use "false") + * + * @return buffer a buffer that contains a pdf dumpable document + */ + public function to_pdf_file_b(bool $rebuild = false): Buffer + { + // We made no updates, so return the original doc + if ($rebuild === false && count($this->_pdf_objects) === 0 && $this->_certificate === null && $this->_appearance === null) { + return new Buffer($this->_buffer); } - // If an image is provided, let's load it - if ($imagefilename !== null) { - // Signature with appearance, following the Adobe workflow: - // 1. form - // 2. layers /n0 (empty) and /n2 - // https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/acrobat_digital_signature_appearances_v9.pdf + // Save the state prior to generating the objects + $this->push_state(); - // Get the page height, to change the coordinates system (up to down) - $pagesize = $this->get_page_size($pagetoappear); - $pagesize = explode(" ", $pagesize[0]->val()); - $pagesize_h = floatval("" . $pagesize[3]) - floatval("" . $pagesize[1]); + // Update the timestamp + $this->update_mod_date(); - $bbox = [ 0, 0, $recttoappear[2] - $recttoappear[0], $recttoappear[3] - $recttoappear[1]]; - $form_object = $this->create_object([ - "BBox" => $bbox, - "Subtype" => "/Form", - "Type" => "/XObject", - "Group" => [ - 'Type' => '/Group', - 'S' => '/Transparency', - 'CS' => '/DeviceRGB' - ] - ]); + $_signature = null; + if ($this->_appearance !== null || $this->_certificate !== null) { + $_signature = $this->_generate_signature_in_document(); + if ($_signature === false) { + $this->pop_state(); - $container_form_object = $this->create_object([ - "BBox" => $bbox, - "Subtype" => "/Form", - "Type" => "/XObject", - "Resources" => [ "XObject" => [ - "n0" => new PDFValueSimple(""), - "n2" => new PDFValueSimple("") - ] ] - ]); - $container_form_object->set_stream("q 1 0 0 1 0 0 cm /n0 Do Q\nq 1 0 0 1 0 0 cm /n2 Do Q\n", false); + throw new PDFException('could not generate the signed document'); + } + } - $layer_n0 = $this->create_object([ - "BBox" => [ 0.0, 0.0, 100.0, 100.0 ], - "Subtype" => "/Form", - "Type" => "/XObject", - "Resources" => new PDFValueObject() - ]); + // Generate the first part of the document + [$_doc_to_xref, $_obj_offsets] = $this->_generate_content_to_xref($rebuild); + $xref_offset = $_doc_to_xref->size(); - // Add the same structure than Acrobat Reader - $layer_n0->set_stream("% DSBlank" . __EOL, false); + if ($_signature instanceof PDFSignatureObject) { + $_obj_offsets[$_signature->get_oid()] = $_doc_to_xref->size(); + $xref_offset += strlen($_signature->to_pdf_entry()); + } - $layer_n2 = $this->create_object([ - "BBox" => $bbox, - "Subtype" => "/Form", - "Type" => "/XObject", - "Resources" => new PDFValueObject() - ]); - - $result = _add_image([$this, "create_object"], $imagefilename, $bbox[0], $bbox[1], $bbox[2], $bbox[3], $page_rotation->val()); - if ($result === false) - return p_error("could not add the image"); - - $layer_n2["Resources"] = $result["resources"]; - $layer_n2->set_stream($result['command'], false); - - $container_form_object["Resources"]["XObject"]["n0"] = new PDFValueReference($layer_n0->get_oid()); - $container_form_object["Resources"]["XObject"]["n2"] = new PDFValueReference($layer_n2->get_oid()); - - $form_object['Resources'] = new PDFValueObject([ - "XObject" => [ - "FRM" => new PDFValueReference($container_form_object->get_oid()) - ] - ]); - $form_object->set_stream("/FRM Do", false); - - // Set the signature appearance field to the form object - $annotation_object["AP"] = [ "N" => new PDFValueReference($form_object->get_oid())]; - $annotation_object["Rect"] = [ $recttoappear[0], $pagesize_h - $recttoappear[1], $recttoappear[2], $pagesize_h - $recttoappear[3] ]; - } - - if (!$newannots->push(new PDFValueReference($annotation_object->get_oid()))) - return p_error("Could not update the page where the signature has to appear"); - - $page_obj["Annots"] = new PDFValueReference($newannots->get_oid()); - array_push($updated_objects, $page_obj); - - // AcroForm may be an indirect object - if (!isset($root_obj["AcroForm"])) - $root_obj["AcroForm"] = new PDFValueObject(); - - $acroform = &$root_obj["AcroForm"]; - if ((($referenced = $acroform->get_object_referenced()) !== false) && (!is_array($referenced))) { - $acroform = $this->get_object($referenced); - array_push($updated_objects, $acroform); - } else { - array_push($updated_objects, $root_obj); - } - - // Add the annotation to the interactive form - $acroform["SigFlags"] = 3; - if (!isset($acroform['Fields'])) - $acroform['Fields'] = new PDFValueList(); - - // Add the annotation object to the interactive form - if (!$acroform['Fields']->push(new PDFValueReference($annotation_object->get_oid()))) { - return p_error("could not create the signature field"); - } - - // Store the objects - foreach ($updated_objects as &$object) { - $this->add_object($object); - } - - return $signature; - } - - /** - * Function that updates the modification date of the document. If modifies two parts: the "info" field of the trailer object - * and the xmp metadata field pointed by the root object. - * @param date a DateTime object that contains the date to be set; null to set "now" - * @return ok true if the date could be set; false otherwise - */ - protected function update_mod_date(\DateTime $date = null) { - // First of all, we are searching for the root object (which should be in the trailer) - $root = $this->_pdf_trailer_object["Root"]; - - if (($root === false) || (($root = $root->get_object_referenced()) === false)) - return p_error("could not find the root object from the trailer"); - - $root_obj = $this->get_object($root); - if ($root_obj === false) - return p_error("invalid root object"); - - if ($date === null) - $date = new \DateTime(); - - // Update the xmp metadata if exists - if (isset($root_obj["Metadata"])) { - $metadata = $root_obj["Metadata"]; - if ((($referenced = $metadata->get_object_referenced()) !== false) && (!is_array($referenced))) { - $metadata = $this->get_object($referenced); - $metastream = $metadata->get_stream(); - $metastream = preg_replace('/([^<]*)<\/xmp:ModifyDate>/', '' . $date->format("c") . '', $metastream); - $metastream = preg_replace('/([^<]*)<\/xmp:MetadataDate>/', '' . $date->format("c") . '', $metastream); - $metastream = preg_replace('/([^<]*)<\/xmpMM:InstanceID>/', 'uuid:' . UUID::v4() . '', $metastream); - $metadata->set_stream($metastream, false); - $this->add_object($metadata); - } - } - - // Update the information object (not really needed) - $info = $this->_pdf_trailer_object["Info"]; - if (($info === false) || (($info = $info->get_object_referenced()) === false)) - return p_error("could not find the info object from the trailer"); - - $info_obj = $this->get_object($info); - if ($info_obj === false) - return p_error("invalid info object"); - - $info_obj["ModDate"] = new PDFValueString(timestamp_to_pdfdatestring($date)); - $info_obj["Producer"] = new PDFValueString("Modificado con SAPP"); - $this->add_object($info_obj); - return true; - } - - /** - * Function that gets the objects that have been read from the document - * @return objects an array of objects, indexed by the oid of each object - */ - public function get_objects() { - return $this->_pdf_objects; - } - - /** - * Function that gets the version of the document. It will have the form - * PDF-1.x - * @return version the PDF version - */ - public function get_version() { - return $this->_pdf_version_string; - } - - /** - * Function that sets the version for the document. - * @param version the version of the PDF document (it shall have the form PDF-1.x) - * @return correct true if the version had the proper form; false otherwise - */ - public function set_version($version) { - if (preg_match("/PDF-1.\[0-9\]/", $version) !== 1) { - return false; - } - $this->_pdf_version_string = $version; - return true; - } - - /** - * Function that creates a new PDFObject and stores it in the document object list, so that - * it is automatically managed by the document. The returned object can be modified and - * that modifications will be reflected in the document. - * @param value the value that the object will contain - * @return obj the PDFObject created - */ - public function create_object($value = [], $class = "ddn\\sapp\\PDFObject", $autoadd = true): PDFObject { - $o = new $class($this->get_new_oid(), $value); - if ($autoadd === true) - $this->add_object($o); - return $o; - } - - /** - * Adds a pdf object to the document (overwrites the one with the same oid, if existed) - * @param pdf_object the object to add to the document - * @return true if the object was added; false otherwise (e.g. already exists an object of a greater generation) - */ - public function add_object(PDFObject $pdf_object) { - $oid = $pdf_object->get_oid(); - - if (isset($this->_pdf_objects[$oid])) { - if ($this->_pdf_objects[$oid]->get_generation() > $pdf_object->get_generation()) { - return false; - } - } - - $this->_pdf_objects[$oid] = $pdf_object; - - // Update the maximum oid - if ($oid > $this->_max_oid) - $this->_max_oid = $oid; - - return true; - } - - /** - * This function generates all the contents of the file up to the xref entry. - * @param rebuild whether to generate the xref with all the objects in the document (true) or - * consider only the new ones (false) - * @return xref_data [ the text corresponding to the objects, array of offsets for each object ] - */ - protected function _generate_content_to_xref($rebuild = false) { - if ($rebuild === true) { - $result = new Buffer("%$this->_pdf_version_string" . __EOL); - } else { - $result = new Buffer($this->_buffer); - } - - // Need to calculate the objects offset - $offsets = []; - $offsets[0] = 0; - - // The objects - $offset = $result->size(); - - if ($rebuild === true) { - for ($i = 0; $i <= $this->_max_oid; $i++) { - if (($object = $this->get_object($i)) === false) continue; - - $result->data($object->to_pdf_entry()); - $offsets[$i] = $offset; - $offset = $result->size(); - } - } else { - foreach ($this->_pdf_objects as $obj_id => $object) { - $result->data($object->to_pdf_entry()); - $offsets[$obj_id] = $offset; - $offset = $result->size(); - } - } - - return [ $result, $offsets ]; - } - - /** - * This functions outputs the document to a buffer object, ready to be dumped to a file. - * @param rebuild whether we are rebuilding the whole xref table or not (in case of incremental versions, we should use "false") - * @return buffer a buffer that contains a pdf dumpable document - */ - public function to_pdf_file_b($rebuild = false) : Buffer { - // We made no updates, so return the original doc - if (($rebuild === false) && (count($this->_pdf_objects) === 0) && ($this->_certificate === null) && ($this->_appearance === null)) - return new Buffer($this->_buffer); - - // Save the state prior to generating the objects - $this->push_state(); - - // Update the timestamp - $this->update_mod_date(); - - $_signature = null; - if (($this->_appearance !== null) || ($this->_certificate !== null)) { - $_signature = $this->_generate_signature_in_document(); - if ($_signature === false) { - $this->pop_state(); - return p_error("could not generate the signed document"); - } - } - - // Generate the first part of the document - [ $_doc_to_xref, $_obj_offsets ] = $this->_generate_content_to_xref($rebuild); - $xref_offset = $_doc_to_xref->size(); - - if ($_signature !== null) { - $_obj_offsets[$_signature->get_oid()] = $_doc_to_xref->size(); - $xref_offset += strlen($_signature->to_pdf_entry()); - } - - $doc_version_string = str_replace("PDF-", "", $this->_pdf_version_string); + $doc_version_string = str_replace('PDF-', '', $this->_pdf_version_string); // The version considered for the cross reference table depends on the version of the current xref table, // as it is not possible to mix xref tables. Anyway we are $target_version = $this->_xref_table_version; - if ($this->_xref_table_version >= "1.5") { + if ($this->_xref_table_version >= '1.5') { // i.e. xref streams - if ($doc_version_string > $target_version) + if ($doc_version_string > $target_version) { $target_version = $doc_version_string; - } else { + } + } elseif ($doc_version_string < $target_version) { // i.e. xref+trailer - if ($doc_version_string < $target_version) - $target_version = $doc_version_string; + $target_version = $doc_version_string; } - if ($target_version >= "1.5") { - p_debug("generating xref using cross-reference streams"); + if ($target_version >= '1.5') { + $this->logger?->debug('generating xref using cross-reference streams'); // Create a new object for the trailer $trailer = $this->create_object( @@ -813,44 +581,50 @@ public function to_pdf_file_b($rebuild = false) : Buffer { $xref = PDFUtilFnc::build_xref_1_5($_obj_offsets); // Set the parameters for the trailer - $trailer["Index"] = explode(" ", $xref["Index"]); - $trailer["W"] = $xref["W"]; - $trailer["Size"] = $this->_max_oid + 1; - $trailer["Type"] = "/XRef"; + $trailer['Index'] = explode(' ', (string) $xref['Index']); + $trailer['W'] = $xref['W']; + $trailer['Size'] = $this->_max_oid + 1; + $trailer['Type'] = '/XRef'; // Not needed to generate new IDs, as in metadata the IDs will be set // $ID1 = md5("" . (new \DateTime())->getTimestamp() . "-" . $this->_xref_position . $xref["stream"]); - $ID2 = md5("" . (new \DateTime())->getTimestamp() . "-" . $this->_xref_position . $this->_pdf_trailer_object); + $ID2 = md5('' . (new DateTime())->getTimestamp() . '-' . $this->_xref_position . $this->_pdf_trailer_object); // $trailer["ID"] = [ new PDFValueHexString($ID1), new PDFValueHexString($ID2) ]; - $trailer["ID"] = [ $trailer["ID"][0], new PDFValueHexString(strtoupper($ID2)) ]; + $trailer['ID'] = [$trailer['ID'][0], new PDFValueHexString(strtoupper($ID2))]; // We are not using predictors nor encoding - if (isset($trailer["DecodeParms"])) unset($trailer["DecodeParms"]); + if (isset($trailer['DecodeParms'])) { + unset($trailer['DecodeParms']); + } // We are not compressing the stream - if (isset($trailer["Filter"])) unset($trailer["Filter"]); - $trailer->set_stream($xref["stream"], false); + if (isset($trailer['Filter'])) { + unset($trailer['Filter']); + } + + $trailer->set_stream($xref['stream'], false); // If creating an incremental modification, point to the previous xref table - if ($rebuild === false) + if ($rebuild === false) { $trailer['Prev'] = $this->_xref_position; - else + } elseif (isset($trailer['Prev'])) { // If rebuilding the document, remove the references to previous xref tables, because it will be only one - if (isset($trailer['Prev'])) - unset($trailer['Prev']); + unset($trailer['Prev']); + } // And generate the part of the document related to the xref $_doc_from_xref = new Buffer($trailer->to_pdf_entry()); - $_doc_from_xref->data("startxref" . __EOL . "$xref_offset" . __EOL ."%%EOF" . __EOL); + $_doc_from_xref->data('startxref' . __EOL . $xref_offset . __EOL . '%%EOF' . __EOL); } else { - p_debug("generating xref using classic xref...trailer"); + $this->logger?->debug('generating xref using classic xref...trailer'); $xref_content = PDFUtilFnc::build_xref($_obj_offsets); // Update the trailer $this->_pdf_trailer_object['Size'] = $this->_max_oid + 1; - if ($rebuild === false) + if ($rebuild === false) { $this->_pdf_trailer_object['Prev'] = $this->_xref_position; + } // Not needed to generate new IDs, as in metadata the IDs may be set // $ID1 = md5("" . (new \DateTime())->getTimestamp() . "-" . $this->_xref_position . $xref_content); @@ -861,95 +635,123 @@ public function to_pdf_file_b($rebuild = false) : Buffer { // Generate the part of the document related to the xref $_doc_from_xref = new Buffer($xref_content); - $_doc_from_xref->data("trailer\n$this->_pdf_trailer_object"); - $_doc_from_xref->data("\nstartxref\n$xref_offset\n%%EOF\n"); + $_doc_from_xref->data("trailer\n" . $this->_pdf_trailer_object); + $_doc_from_xref->data("\nstartxref\n{$xref_offset}\n%%EOF\n"); } - if ($_signature !== null) { + if ($_signature instanceof PDFSignatureObject) { // In case that the document is signed, calculate the signature $_signature->set_sizes($_doc_to_xref->size(), $_doc_from_xref->size()); - $_signature["Contents"] = new PDFValueSimple(""); + $_signature['Contents'] = new PDFValueSimple(''); + assert($_signature instanceof PDFSignatureObject); $_signable_document = new Buffer($_doc_to_xref->get_raw() . $_signature->to_pdf_entry() . $_doc_from_xref->get_raw()); $certificate = $_signature->get_certificate(); - $extracerts = (array_key_exists('extracerts', $certificate)) ? $certificate['extracerts'] : null; - $cms = new CMS; + $extracerts = $certificate['extracerts'] ?? null; + $cms = new CMS($this->logger); $cms->signature_data['hashAlgorithm'] = 'sha256'; $cms->signature_data['privkey'] = $certificate['pkey']; $cms->signature_data['extracerts'] = $extracerts; - $cms->signature_data['signcert'] = $certificate['cert']; + $cms->signature_data['signcert'] = $certificate['cert']; $cms->signature_data['ltv'] = $_signature->get_ltv(); $cms->signature_data['tsa'] = $_signature->get_tsa(); $signature_contents = $cms->pkcs7_sign($_signable_document->get_raw()); //$signature_contents = str_pad($signature_contents, strlen($signature_contents), '0'); // Then restore the contents field - $_signature["Contents"] = new PDFValueHexString($signature_contents); + $_signature['Contents'] = new PDFValueHexString($signature_contents); // Add this object to the content previous to this document xref + assert($_signature instanceof PDFSignatureObject); $_doc_to_xref->data($_signature->to_pdf_entry()); } // Reset the state to make signature objects not to mess with the user's objects $this->pop_state(); + return new Buffer($_doc_to_xref->get_raw() . $_doc_from_xref->get_raw()); } /** * This functions outputs the document to a string, ready to be written + * * @return buffer a buffer that contains a pdf document */ - public function to_pdf_file_s($rebuild = false) { - $pdf_content = $this->to_pdf_file_b($rebuild); - return $pdf_content->get_raw(); + public function to_pdf_file_s(bool $rebuild = false): ?string + { + return $this->to_pdf_file_b($rebuild)->get_raw(); } /** * This function writes the document to a file + * * @param filename the name of the file to be written (it will be overwritten, if exists) + * * @return written true if the file has been correcly written to the file; false otherwise */ - public function to_pdf_file($filename, $rebuild = false) { + public function to_pdf_file($filename, bool $rebuild = false): bool + { $pdf_content = $this->to_pdf_file_b($rebuild); - $file = fopen($filename, "wb"); + $file = fopen($filename, 'wb'); if ($file === false) { - return p_error("failed to create the file"); + throw new PDFException('failed to create the file'); } + if (fwrite($file, $pdf_content->get_raw()) !== $pdf_content->size()) { fclose($file); - return p_error("failed to write to file"); + + throw new PDFException('failed to write to file'); } + fclose($file); + return true; } /** * Gets the page object which is rendered in position i + * * @param i the number of page (according to the rendering order) + * * @return page the page object */ - public function get_page($i) { - if ($i < 0) return false; - if ($i >= count($this->_pages_info)) return false; + public function get_page(int $i): PDFObject|false + { + if ($i < 0) { + return false; + } + + if ($i >= count($this->_pages_info)) { + return false; + } + return $this->get_object($this->_pages_info[$i]['id']); } /** * Gets the size of the page in the form of a rectangle [ x0 y0 x1 y1 ] + * * @param i the number of page (according to the rendering order), or the page object + * * @return box the bounding box of the page */ - public function get_page_size($i) { + public function get_page_size(int $i): false|array + { $pageinfo = false; if (is_int($i)) { - if ($i < 0) return false; - if ($i > count($this->_pages_info)) return false; + if ($i < 0) { + return false; + } + + if ($i > count($this->_pages_info)) { + return false; + } $pageinfo = $this->_pages_info[$i]['info']; } else { - foreach ($this->_pages_info as $k => $info) { + foreach ($this->_pages_info as $info) { if ($info['oid'] === $i->get_oid()) { $pageinfo = $info['info']; break; @@ -958,123 +760,69 @@ public function get_page_size($i) { } // The page has not been found - if (($pageinfo === false) || (!isset($pageinfo['size']))) + if ($pageinfo === false || ! isset($pageinfo['size'])) { return false; + } return $pageinfo['size']; } /** - * This function builds the page IDs for object with id oid. If it is a page, it returns the oid; if it is not and it has - * kids and every kid is a page (or a set of pages), it finds the pages. - * @param oid the object id to inspect - * @return pages the ordered list of page ids corresponding to object oid, or false if any of the kid objects - * is not of type page or pages. + * This function compares this document with other document, object by object. The idea is to compare the objects with the same oid in the + * different documents, checking field by field; it does not take into account the streams. + * + * @return PDFObject[] */ - protected function _get_page_info($oid, $info = []) { - $object = $this->get_object($oid); - if ($object === false) - return p_error("could not get information about the page"); + public function compare($other): array + { + $other_objects = []; + foreach ($other->get_object_iterator(false) as $oid => $object) { + $other_objects[$oid] = $object; + } - $page_ids = []; + $differences = []; - switch ($object["Type"]->val()) { - case "Pages": - $kids = $object["Kids"]; - $kids = $kids->get_object_referenced(); - if ($kids !== false) { - if (isset($object['MediaBox'])) { - $info['size'] = $object['MediaBox']->val(); - } - foreach ($kids as $kid) { - $ids = $this->_get_page_info($kid, $info); - if ($ids === false) - return false; - array_push($page_ids, ...$ids); - } - } else { - return p_error("could not get the pages"); + foreach ($this->get_object_iterator(false) as $oid => $object) { + if (isset($other_objects[$oid])) { + // The object exists, so we need to compare + $diff = $object->get_value()->diff($other_objects[$oid]->get_value()); + if ($diff !== null) { + $differences[$oid] = new PDFObject($oid, $diff); } - break; - case "Page": - if (isset($object['MediaBox'])) - $info['size'] = $object['MediaBox']->val(); - return [ [ 'id' => $oid, 'info' => $info ] ]; - default: - return false; + } else { + $differences[$oid] = new PDFObject($oid, $object->get_value()); + } } - return $page_ids; - } - - /** - * Obtains an ordered list of objects that contain the ids of the page objects of the document. - * The order is made according to the catalog and the document structure. - * @return list an ordered list of the id of the page objects, or false if could not be found - */ - protected function _acquire_pages_info() { - $root = $this->_pdf_trailer_object["Root"]; - if (($root === false) || (($root = $root->get_object_referenced()) === false)) - return p_error("could not find the root object from the trailer"); - - $root = $this->get_object($root); - if ($root !== false) { - $pages = $root["Pages"]; - if (($pages === false) || (($pages = $pages->get_object_referenced()) === false)) - return p_error("could not find the pages for the document"); - - $this->_pages_info = $this->_get_page_info($pages); - } else - p_warning("root object does not exist, so cannot get information about pages"); - } - - - /** - * This function compares this document with other document, object by object. The idea is to compare the objects with the same oid in the - * different documents, checking field by field; it does not take into account the streams. - */ - public function compare($other) { - $other_objects = []; - foreach ($other->get_object_iterator(false) as $oid => $object) { - $other_objects[$oid] = $object; - } - - $differences = []; - - foreach ($this->get_object_iterator(false) as $oid => $object) { - if (isset($other_objects[$oid])) { - // The object exists, so we need to compare - $diff = $object->get_value()->diff($other_objects[$oid]->get_value()); - if ($diff !== null) { - $differences[$oid] = new PDFObject($oid, $diff); - } - } else { - $differences[$oid] = new PDFObject($oid, $object->get_value()); - } - } return $differences; } /** * Obtains the tree of objects in the PDF Document. The result is an array of DependencyTreeObject objects (indexed by the oid), where * each element has a set of children that can be retrieved using the iterator (foreach $o->children() as $oid => $object ...) + * + * @return DependencyTreeObject[] */ - public function get_object_tree() { - + public function get_object_tree(): array + { // Prepare the return value $objects = []; foreach ($this->_xref_table as $oid => $offset) { - if ($offset === null) continue; + if ($offset === null) { + continue; + } $o = $this->get_object($oid); - if ($o === false) continue; + if ($o === false) { + continue; + } - // foreach ($this->get_object_iterator() as $oid => $o) { + // foreach ($this->get_object_iterator() as $oid => $o) { // Create the object in the dependency tree and add it to the list of objects if (! array_key_exists($oid, $objects)) { - $objects[$oid] = new DependencyTreeObject($oid, $o["Type"]); + $objects[$oid] = new DependencyTreeObject($oid, $o['Type']); } // The object is a PDFObject so we need the PDFValueObject to get the value of the fields @@ -1082,29 +830,34 @@ public function get_object_tree() { $val = $o->get_value(); // We'll only consider those objects that may create an structure (i.e. the objects, whose fields may include references to other objects) - if (is_a($val, "ddn\\sapp\\pdfvalue\\PDFValueObject")) { - $references = references_in_object($val, $oid); + if (is_a($val, PDFValueObject::class)) { + $references = references_in_object($val); } else { $references = $val->get_object_referenced(); - if ($references === false) + if ($references === false) { continue; - if (!is_array($references)) $references = [ $references ]; + } + + if (! is_array($references)) { + $references = [$references]; + } } - // p_debug("$oid references " . implode(", ", $references)); + // $this->logger?->debug("$oid references " . implode(", ", $references)); foreach ($references as $r_object) { if (! array_key_exists($r_object, $objects)) { $r_object_o = $this->get_object($r_object); - $objects[$r_object] = new DependencyTreeObject($r_object, $r_object_o["Type"]); + $objects[$r_object] = new DependencyTreeObject($r_object, $r_object_o['Type']); } + $object->addchild($r_object, $objects[$r_object]); } } // $xref_children = []; - foreach ($objects as $oid => $t_object) { - if ($t_object->info == "/XRef") { + foreach ($objects as $t_object) { + if ($t_object->info === '/XRef') { array_push($xref_children, ...iterator_to_array($t_object->children())); } } @@ -1113,49 +866,61 @@ public function get_object_tree() { // Remove those objects that are child of other objects from the top of the tree foreach ($objects as $oid => $t_object) { - if (($t_object->is_child > 0) || (in_array($t_object->info, [ "/XRef", "/ObjStm"] ))) { - if (! in_array($oid, $xref_children)) - unset($objects[$oid]); + if (($t_object->is_child > 0 || in_array($t_object->info, ['/XRef', '/ObjStm'], true)) && ! in_array($oid, $xref_children, true)) { + unset($objects[$oid]); } } return $objects; } - /** * Retrieve the signatures in the document + * * @return array of signatures in the original document */ - public function get_signatures() { - + public function get_signatures(): array + { // Prepare the return value $signatures = []; foreach ($this->_xref_table as $oid => $offset) { - if ($offset === null) continue; + if ($offset === null) { + continue; + } $o = $this->get_object($oid); - if ($o === false) continue; + if ($o === false) { + continue; + } $o_value = $o->get_value()->val(); - if (! is_array($o_value) || ! isset($o_value['Type'])) continue; - if ($o_value['Type']->val() != 'Sig') continue; + if (! is_array($o_value) || ! isset($o_value['Type'])) { + continue; + } + + if ($o_value['Type']->val() !== 'Sig') { + continue; + } - $signature = ['content' => $o_value['Contents']->val()]; + $signature = [ + 'content' => $o_value['Contents']->val(), + ]; try { - $cert=[]; + $cert = []; openssl_pkcs7_read( "-----BEGIN CERTIFICATE-----\n" - . chunk_split(base64_encode(hex2bin($signature['content'])), 64, "\n") - . "-----END CERTIFICATE-----\n", - $cert + . chunk_split(base64_encode(hex2bin((string) $signature['content'])), 64, "\n") + . "-----END CERTIFICATE-----\n", + $cert ); - $signature += openssl_x509_parse($cert[0] ?? '') ?: []; - } catch (\Throwable $e) {} + $signature += openssl_x509_parse($cert[0] ?? ''); + } catch (Throwable $e) { + throw new PDFException('failed to read certificate', 0, $e); + } $signatures[] = $signature; } @@ -1165,16 +930,18 @@ public function get_signatures() { /** * Retrieve the number of signatures in the document + * * @return int signatures number in the original document */ - public function get_signature_count() { + public function get_signature_count(): int + { return count($this->get_signatures()); } - /** * Generates a new document that is the result of signing the current * document + * * @param certfile a file that contains a user certificate in pkcs12 format, or an array [ 'cert' => , 'pkey' => ] * that would be the output of openssl_pkcs12_read * @param password the password to read the private key @@ -1188,68 +955,503 @@ public function get_signature_count() { * - if array [ width, height ], it will be the width and the height for the image to be included as a signature appearance (if * one of these values is null, it will fallback to the actual width or height of the image) */ - public function sign_document($certfile, $password = null, $page_to_appear = 0, $imagefilename = null, $px = 0, $py = 0, $size = null) { - + public function sign_document($certfile, $password = null, $page_to_appear = 0, $imagefilename = null, $px = 0, $py = 0, $size = null): self + { if ($imagefilename !== null) { - $position = [ ]; $imagesize = @getimagesize($imagefilename); if ($imagesize === false) { - return p_warning("failed to open the image $image"); + throw new PDFException('failed to open the image ' . $imagesize); } - if (($page_to_appear < 0) || ($page_to_appear > $this->get_page_count() - 1)) { - return p_error("invalid page number"); + + if ($page_to_appear < 0 || $page_to_appear > $this->get_page_count() - 1) { + throw new PDFException('invalid page number'); } + $pagesize = $this->get_page_size($page_to_appear); if ($pagesize === false) { - return p_error("failed to get page size"); + throw new PDFException('failed to get page size'); } - $pagesize = explode(" ", $pagesize[0]->val()); + $pagesize = explode(' ', (string) $pagesize[0]->val()); // Get the bounding box for the image - $p_x = intval("". $pagesize[0]); - $p_y = intval("". $pagesize[1]); - $p_w = intval("". $pagesize[2]) - $p_x; - $p_h = intval("". $pagesize[3]) - $p_y; + $p_x = (int) $pagesize[0]; + $p_y = (int) $pagesize[1]; // Add the position for the image - $p_x = $p_x + $px; - $p_y = $p_y + $py; + $p_x += $px; + $p_y += $py; $i_w = $imagesize[0]; $i_h = $imagesize[1]; if (is_array($size)) { if (count($size) != 2) { - return p_error("invalid size"); + throw new PDFException('invalid size'); } + $width = $size[0]; $height = $size[1]; - } else if ($size === null) { + } elseif ($size === null) { $width = $i_w; $height = $i_h; - } else if (is_float($size) || is_int($size)) { + } elseif (is_float($size) || is_int($size)) { $width = $i_w * $size; $height = $i_h * $size; } else { - return p_error("invalid size format"); + throw new PDFException('invalid size format'); } - $i_w = $width===null?$imagesize[0]:$width; - $i_h = $height===null?$imagesize[1]:$height; + $i_w = $width ?? $imagesize[0]; + $i_h = $height ?? $imagesize[1]; // Set the image appearance and the certificate file - $this->set_signature_appearance($page_to_appear, [ $p_x, $p_y, $p_x + $i_w, $p_y + $i_h ], $imagefilename); + $this->set_signature_appearance($page_to_appear, [$p_x, $p_y, $p_x + $i_w, $p_y + $i_h], $imagefilename); } - if (!$this->set_signature_certificate($certfile, $password)) { - return p_error("the certificate or the signature is not valid"); + if (! $this->set_signature_certificate($certfile, $password)) { + throw new PDFException('the certificate or the signature is not valid'); } $docsigned = $this->to_pdf_file_s(); if ($docsigned === false) { - return p_error("failed to sign the document"); + throw new PDFException('failed to sign the document'); + } + + return self::from_string($docsigned); + } + + // Gets a new oid for a new object + protected function get_new_oid(): int|float + { + $this->_max_oid++; + + return $this->_max_oid; + } + + /** + * Function that creates and updates the PDF objects needed to sign the document. The workflow for a signature is: + * - create a signature object + * - create an annotation object whose value is the signature object + * - create a form object (along with other objects) that will hold the appearance of the annotation object + * - modify the root object to make acroform point to the annotation object + * - modify the page object to make the annotations of that page include the annotation object + * > If the appearance is not set, the image will not appear, and the signature object will be invisible. + * > If the certificate is not set, the signature created will be a placeholder (that acrobat will able to sign) + * LIMITATIONS: one document can be signed once at a time; if wanted more signatures, then chain the documents: + * $o1->set_signature_certificate(...); + * $o2 = PDFDoc::fromstring($o1->to_pdf_file_s); + * $o2->set_signature_certificate(...); + * $o2->to_pdf_file_s(); + * + * @return signature a signature object, or null if the document is not signed; false if an error happens + */ + protected function _generate_signature_in_document(): PDFSignatureObject|false + { + $imagefilename = null; + $recttoappear = [0, 0, 0, 0]; + $pagetoappear = 0; + + if ($this->_appearance !== null) { + $imagefilename = $this->_appearance['image']; + $recttoappear = $this->_appearance['rect']; + $pagetoappear = $this->_appearance['page']; + } + + // First of all, we are searching for the root object (which should be in the trailer) + $root = $this->_pdf_trailer_object['Root']; + + if ($root === false || ($root = $root->get_object_referenced()) === false) { + throw new PDFException('could not find the root object from the trailer'); + } + + $root_obj = $this->get_object($root); + if ($root_obj === false) { + throw new PDFException('invalid root object'); + } + + // Now the object corresponding to the page number in which to appear + $page_obj = $this->get_page($pagetoappear); + if ($page_obj === false) { + throw new PDFException('invalid page'); + } + + assert($page_obj instanceof PDFObject); + // The objects to update + $updated_objects = []; + + // Add the annotation to the page + if (! isset($page_obj['Annots'])) { + $page_obj['Annots'] = new PDFValueList(); + } + + $annots = &$page_obj['Annots']; + $page_rotation = $page_obj['Rotate'] ?? new PDFValueSimple(0); + + if (($referenced = $annots->get_object_referenced()) !== false && ! is_array($referenced)) { + // It is an indirect object, so we need to update that object + $newannots = $this->create_object( + $this->get_object($referenced)->get_value() + ); + } else { + $newannots = $this->create_object( + new PDFValueList() + ); + $newannots->push($annots); + } + + // Create the annotation object, annotate the offset and append the object + $annotation_object = $this->create_object( + [ + 'Type' => '/Annot', + 'Subtype' => '/Widget', + 'FT' => '/Sig', + 'V' => new PDFValueString(''), + 'T' => new PDFValueString('Signature' . get_random_string()), + 'P' => new PDFValueReference($page_obj->get_oid()), + 'Rect' => $recttoappear, + 'F' => 132, // TODO: check this value + ] + ); + + // Prepare the signature object (we need references to it) + $signature = null; + if ($this->_certificate !== null) { + // Perform signature test to get signature size to define __SIGNATURE_MAX_LENGTH + $this->logger?->debug(" ########## PERFORM SIGNATURE LENGTH CHECK ##########\n"); + $CMS = new CMS($this->logger); + $CMS->signature_data['signcert'] = $this->_certificate['cert']; + $CMS->signature_data['extracerts'] = $this->_certificate['extracerts'] ?? null; + $CMS->signature_data['hashAlgorithm'] = 'sha256'; + $CMS->signature_data['privkey'] = $this->_certificate['pkey']; + $CMS->signature_data['tsa'] = $this->_signature_tsa; + $CMS->signature_data['ltv'] = $this->_signature_ltv_data; + $res = $CMS->pkcs7_sign('0'); + $len = strlen($res); + $this->logger?->debug(sprintf(' Signature Length is "%d" Bytes', $len)); + $this->logger?->debug(" ########## FINISHED SIGNATURE LENGTH CHECK #########\n\n"); + PDFSignatureObject::$__SIGNATURE_MAX_LENGTH = $len; + + $signature = $this->create_object([], PDFSignatureObject::class, false); + //$signature = new PDFSignatureObject([]); + $signature->set_metadata($this->_metadata_name, $this->_metadata_reason, $this->_metadata_location, $this->_metadata_contact_info); + $signature->set_certificate($this->_certificate); + if ($this->_signature_tsa !== null) { + $signature->set_signature_tsa($this->_signature_tsa); + } + + if ($this->_signature_ltv_data !== null) { + $signature->set_signature_ltv($this->_signature_ltv_data); + } + + // Update the value to the annotation object + $annotation_object['V'] = new PDFValueReference($signature->get_oid()); } - return PDFDoc::from_string($docsigned); + + // If an image is provided, let's load it + if ($imagefilename !== null) { + // Signature with appearance, following the Adobe workflow: + // 1. form + // 2. layers /n0 (empty) and /n2 + // https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/acrobat_digital_signature_appearances_v9.pdf + + // Get the page height, to change the coordinates system (up to down) + $pagesize = $this->get_page_size($pagetoappear); + $pagesize = explode(' ', (string) $pagesize[0]->val()); + $pagesize_h = (float) ('' . $pagesize[3]) - (float) ('' . $pagesize[1]); + + $bbox = [0, 0, $recttoappear[2] - $recttoappear[0], $recttoappear[3] - $recttoappear[1]]; + $form_object = $this->create_object([ + 'BBox' => $bbox, + 'Subtype' => '/Form', + 'Type' => '/XObject', + 'Group' => [ + 'Type' => '/Group', + 'S' => '/Transparency', + 'CS' => '/DeviceRGB', + ], + ]); + + $container_form_object = $this->create_object([ + 'BBox' => $bbox, + 'Subtype' => '/Form', + 'Type' => '/XObject', + 'Resources' => [ + 'XObject' => [ + 'n0' => new PDFValueSimple(''), + 'n2' => new PDFValueSimple(''), + ], + ], + ]); + $container_form_object->set_stream("q 1 0 0 1 0 0 cm /n0 Do Q\nq 1 0 0 1 0 0 cm /n2 Do Q\n", false); + + $layer_n0 = $this->create_object([ + 'BBox' => [0.0, 0.0, 100.0, 100.0], + 'Subtype' => '/Form', + 'Type' => '/XObject', + 'Resources' => new PDFValueObject(), + ]); + + // Add the same structure than Acrobat Reader + $layer_n0->set_stream('% DSBlank' . __EOL, false); + + $layer_n2 = $this->create_object([ + 'BBox' => $bbox, + 'Subtype' => '/Form', + 'Type' => '/XObject', + 'Resources' => new PDFValueObject(), + ]); + + $result = _add_image($this->create_object(...), $imagefilename, $bbox[0], $bbox[1], $bbox[2], $bbox[3], $page_rotation->val()); + if ($result === false) { + throw new PDFException('could not add the image'); + } + + $layer_n2['Resources'] = $result['resources']; + $layer_n2->set_stream($result['command'], false); + + $container_form_object['Resources']['XObject']['n0'] = new PDFValueReference($layer_n0->get_oid()); + $container_form_object['Resources']['XObject']['n2'] = new PDFValueReference($layer_n2->get_oid()); + + assert($container_form_object instanceof PDFObject); + $form_object['Resources'] = new PDFValueObject([ + 'XObject' => [ + 'FRM' => new PDFValueReference($container_form_object->get_oid()), + ], + ]); + $form_object->set_stream('/FRM Do', false); + + // Set the signature appearance field to the form object + $annotation_object['AP'] = [ + 'N' => new PDFValueReference($form_object->get_oid()), + ]; + $annotation_object['Rect'] = [$recttoappear[0], $pagesize_h - $recttoappear[1], $recttoappear[2], $pagesize_h - $recttoappear[3]]; + } + + if (! $newannots->push(new PDFValueReference($annotation_object->get_oid()))) { + throw new PDFException('Could not update the page where the signature has to appear'); + } + + $page_obj['Annots'] = new PDFValueReference($newannots->get_oid()); + $updated_objects[] = $page_obj; + + // AcroForm may be an indirect object + if (! isset($root_obj['AcroForm'])) { + $root_obj['AcroForm'] = new PDFValueObject(); + } + + $acroform = &$root_obj['AcroForm']; + if (($referenced = $acroform->get_object_referenced()) !== false && ! is_array($referenced)) { + $acroform = $this->get_object($referenced); + $updated_objects[] = $acroform; + } else { + $updated_objects[] = $root_obj; + } + + // Add the annotation to the interactive form + $acroform['SigFlags'] = 3; + if (! isset($acroform['Fields'])) { + $acroform['Fields'] = new PDFValueList(); + } + + // Add the annotation object to the interactive form + if (! $acroform['Fields']->push(new PDFValueReference($annotation_object->get_oid()))) { + throw new PDFException('could not create the signature field'); + } + + // Store the objects + foreach ($updated_objects as $object) { + $this->add_object($object); + } + + return $signature; + } + + /** + * Function that updates the modification date of the document. If modifies two parts: the "info" field of the trailer object + * and the xmp metadata field pointed by the root object. + * + * @param date a DateTime object that contains the date to be set; null to set "now" + * + * @return ok true if the date could be set; false otherwise + */ + protected function update_mod_date(?DateTime $date = null): bool + { + // First of all, we are searching for the root object (which should be in the trailer) + $root = $this->_pdf_trailer_object['Root']; + + if ($root === false || ($root = $root->get_object_referenced()) === false) { + throw new PDFException('could not find the root object from the trailer'); + } + + $root_obj = $this->get_object($root); + if ($root_obj === false) { + throw new PDFException('invalid root object'); + } + + if (! $date instanceof DateTime) { + $date = new DateTime(); + } + + // Update the xmp metadata if exists + if (isset($root_obj['Metadata'])) { + $metadata = $root_obj['Metadata']; + if (($referenced = $metadata->get_object_referenced()) !== false && ! is_array($referenced)) { + $metadata = $this->get_object($referenced); + $metastream = $metadata->get_stream(); + $metastream = preg_replace('/([^<]*)<\/xmp:ModifyDate>/', '' . $date->format('c') . '', (string) $metastream); + $metastream = preg_replace('/([^<]*)<\/xmp:MetadataDate>/', '' . $date->format('c') . '', (string) $metastream); + $metastream = preg_replace('/([^<]*)<\/xmpMM:InstanceID>/', 'uuid:' . UUID::v4() . '', (string) $metastream); + $metadata->set_stream($metastream, false); + $this->add_object($metadata); + } + } + + // Update the information object (not really needed) + $info = $this->_pdf_trailer_object['Info']; + if ($info === false || ($info = $info->get_object_referenced()) === false) { + throw new PDFException('could not find the info object from the trailer'); + } + + $info_obj = $this->get_object($info); + if ($info_obj === false) { + throw new PDFException('invalid info object'); + } + + $info_obj['ModDate'] = new PDFValueString(timestamp_to_pdfdatestring($date)); + $info_obj['Producer'] = new PDFValueString('Modificado con SAPP'); + $this->add_object($info_obj); + + return true; + } + + /** + * This function generates all the contents of the file up to the xref entry. + * + * @param rebuild whether to generate the xref with all the objects in the document (true) or + * consider only the new ones (false) + * + * @return xref_data [ the text corresponding to the objects, array of offsets for each object ] + */ + protected function _generate_content_to_xref(bool $rebuild = false): array + { + if ($rebuild) { + $result = new Buffer('%' . $this->_pdf_version_string . __EOL); + } else { + $result = new Buffer($this->_buffer); + } + + // Need to calculate the objects offset + $offsets = []; + $offsets[0] = 0; + + // The objects + $offset = $result->size(); + + if ($rebuild) { + for ($i = 0; $i <= $this->_max_oid; $i++) { + if (($object = $this->get_object($i)) === false) { + continue; + } + + $result->data($object->to_pdf_entry()); + $offsets[$i] = $offset; + $offset = $result->size(); + } + } else { + foreach ($this->_pdf_objects as $obj_id => $object) { + $result->data($object->to_pdf_entry()); + $offsets[$obj_id] = $offset; + $offset = $result->size(); + } + } + + return [$result, $offsets]; + } + + /** + * This function builds the page IDs for object with id oid. If it is a page, it returns the oid; if it is not and it has + * kids and every kid is a page (or a set of pages), it finds the pages. + * + * @param oid the object id to inspect + * + * @return pages the ordered list of page ids corresponding to object oid, or false if any of the kid objects + * is not of type page or pages. + */ + protected function _get_page_info(int $oid, array $info = []): array + { + $object = $this->get_object($oid); + if ($object === false) { + throw new PDFException('could not get information about the page'); + } + + $page_ids = []; + + switch ($object['Type']->val()) { + case 'Pages': + $kids = $object['Kids']; + $kids = $kids->get_object_referenced(); + if ($kids !== false) { + if (isset($object['MediaBox'])) { + $info['size'] = $object['MediaBox']->val(); + } + + foreach ($kids as $kid) { + $ids = $this->_get_page_info($kid, $info); + if ($ids === false) { + return false; + } + + array_push($page_ids, ...$ids); + } + } else { + throw new PDFException('could not get the pages'); + } + + break; + case 'Page': + if (isset($object['MediaBox'])) { + $info['size'] = $object['MediaBox']->val(); + } + + return [ + [ + 'id' => $oid, + 'info' => $info, + ], + ]; + default: + return false; + } + + return $page_ids; + } + + /** + * Obtains an ordered list of objects that contain the ids of the page objects of the document. + * The order is made according to the catalog and the document structure. + * + * @return list an ordered list of the id of the page objects, or false if could not be found + */ + protected function _acquire_pages_info(): array + { + $root = $this->_pdf_trailer_object['Root']; + if ($root === false || ($root = $root->get_object_referenced()) === false) { + throw new PDFException('could not find the root object from the trailer'); + } + + $root = $this->get_object($root); + if ($root !== false) { + $pages = $root['Pages']; + if ($pages === false || ($pages = $pages->get_object_referenced()) === false) { + throw new PDFException('could not find the pages for the document'); + } + + $this->_pages_info = $this->_get_page_info($pages); + } else { + $this->logger?->warning('root object does not exist, so cannot get information about pages'); + } + + return []; } } diff --git a/src/PDFDocWithContents.php b/src/PDFDocWithContents.php index 530a50a..f6aa626 100644 --- a/src/PDFDocWithContents.php +++ b/src/PDFDocWithContents.php @@ -21,146 +21,147 @@ namespace ddn\sapp; -use ddn\sapp\PDFDoc; -use ddn\sapp\PDFBaseObject; -use ddn\sapp\pdfvalue\PDFValueObject; use ddn\sapp\pdfvalue\PDFValueList; +use ddn\sapp\pdfvalue\PDFValueObject; use ddn\sapp\pdfvalue\PDFValueReference; -use ddn\sapp\pdfvalue\PDFValueType; -use ddn\sapp\pdfvalue\PDFValueSimple; -use ddn\sapp\pdfvalue\PDFValueHexString; -use ddn\sapp\pdfvalue\PDFValueString; -use ddn\sapp\helpers\Buffer; - -use function ddn\sapp\helpers\get_random_string; -use function ddn\sapp\helpers\p_debug; -use function ddn\sapp\helpers\p_error; -use function ddn\sapp\helpers\p_warning; -use function ddn\sapp\helpers\p_debug_var; use function ddn\sapp\helpers\_add_image; +use function ddn\sapp\helpers\get_random_string; -class PDFDocWithContents extends PDFDoc { - - const T_STANDARD_FONTS = [ - "Times-Roman", - "Times-Bold", - "Time-Italic", - "Time-BoldItalic", - "Courier", - "Courier-Bold", - "Courier-Oblique", - "Courier-BoldOblique", - "Helvetica", - "Helvetica-Bold", - "Helvetica-Oblique", - "Helvetica-BoldOblique", - "Symbol", - "ZapfDingbats" +class PDFDocWithContents extends PDFDoc +{ + public const T_STANDARD_FONTS = [ + 'Times-Roman', + 'Times-Bold', + 'Time-Italic', + 'Time-BoldItalic', + 'Courier', + 'Courier-Bold', + 'Courier-Oblique', + 'Courier-BoldOblique', + 'Helvetica', + 'Helvetica-Bold', + 'Helvetica-Oblique', + 'Helvetica-BoldOblique', + 'Symbol', + 'ZapfDingbats', ]; /** * This is a function that allows to add a very basic text to a page, using a standard font. * The function is mainly oriented to add banners and so on, and not to use for writting. + * * @param page the number of page in which the text should appear * @param text the text * @param x the x offset from left for the text (we do not take care of margins) * @param y the y offset from top for the text (we do not take care of margins) - * @param params an array of values [ "font" => , "size" => , + * @param params an array of values [ "font" => , "size" => , * "color" => <#hexcolor>, "angle" => ] */ - public function add_text($page_to_appear, $text, $x, $y, $params = []) { + public function add_text(int $page_to_appear, $text, $x, $y, $params = []): void + { // TODO: maybe we can create a function that "adds content to a page", and that // function will search for the content field and merge the resources, if // needed - p_warning("This function still needs work"); + $this->logger?->warning('This function still needs work'); $default = [ - "font" => "Helvetica", - "size" => 24, - "color" => "#000000", - "angle" => 0 + 'font' => 'Helvetica', + 'size' => 24, + 'color' => '#000000', + 'angle' => 0, ]; $params = array_merge($default, $params); $page_obj = $this->get_page($page_to_appear); - if ($page_obj === false) - return p_error("invalid page"); + if ($page_obj === false) { + throw new PDFException('invalid page'); + } $resources_obj = $this->get_indirect_object($page_obj['Resources']); - if (array_search($params["font"], self::T_STANDARD_FONTS) === false) - return p_error("only standard fonts are allowed Times-Roman, Helvetica, Courier, Symbol, ZapfDingbats"); + if (! in_array($params['font'], self::T_STANDARD_FONTS, true)) { + throw new PDFException('only standard fonts are allowed Times-Roman, Helvetica, Courier, Symbol, ZapfDingbats'); + } - $font_id = "F" . get_random_string(4); + $font_id = 'F' . get_random_string(4); $resources_obj['Font'][$font_id] = [ - "Type" => "/Font", - "Subtype" => "/Type1", - "BaseFont" => "/" . $params['font'], + 'Type' => '/Font', + 'Subtype' => '/Type1', + 'BaseFont' => '/' . $params['font'], ]; // Get the contents for the page $contents_obj = $this->get_indirect_object($page_obj['Contents']); $data = $contents_obj->get_stream(false); - if ($data === false) - return p_error("could not interpret the contents of the page"); + if ($data === false) { + throw new PDFException('could not interpret the contents of the page'); + } // Get the page height, to change the coordinates system (up to down) $pagesize = $this->get_page_size($page_to_appear); - $pagesize_h = floatval("" . $pagesize[3]) - floatval("" . $pagesize[1]); + $pagesize_h = (float) ('' . $pagesize[3]) - (float) ('' . $pagesize[1]); - $angle = $params["angle"]; - $angle *= M_PI/180; + $angle = $params['angle']; + $angle *= M_PI / 180; $c = cos($angle); $s = sin($angle); $cx = $x; - $cy = ($pagesize_h - $y); + $cy = $pagesize_h - $y; - if ($angle !== 0) - $rotate_command = sprintf("%.5F %.5F %.5F %.5F %.2F %.2F cm 1 0 0 1 %.2F %.2F cm", $c, $s, -$s, $c, $cx, $cy, -$cx, -$cy); + $rotate_command = ''; + if ($angle != 0) { + $rotate_command = sprintf('%.5F %.5F %.5F %.5F %.2F %.2F cm 1 0 0 1 %.2F %.2F cm', $c, $s, -$s, $c, $cx, $cy, -$cx, -$cy); + } - $text_command = "BT "; - $text_command .= "/$font_id " . $params['size'] . " Tf "; - $text_command .= sprintf("%.2f %.2f Td ", $x, $pagesize_h - $y); // Ubicar en x, y - $text_command .= sprintf("(%s) Tj ", $text); - $text_command .= "ET "; + $text_command = 'BT '; + $text_command .= sprintf('/%s ', $font_id) . $params['size'] . ' Tf '; + $text_command .= sprintf('%.2f %.2f Td ', $x, $pagesize_h - $y); // Ubicar en x, y + $text_command .= sprintf('(%s) Tj ', $text); + $text_command .= 'ET '; - $color = $params["color"]; + $color = $params['color']; if ($color[0] === '#') { $colorvalid = true; $r = null; - switch (strlen($color)) { + switch (strlen((string) $color)) { case 4: - $color = "#" . $color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3]; + $color = '#' . $color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3]; + // no break case 7: - list($r, $g, $b) = sscanf($color, "#%02x%02x%02x"); + [$r, $g, $b] = sscanf($color, '#%02x%02x%02x'); break; default: - p_error("please use html-like colors (e.g. #ffbbaa)"); + throw new PDFException('please use html-like colors (e.g. #ffbbaa)'); } - if ($r !== null) - $text_command = " q $r $g $b rg $text_command Q"; // Color RGB - } else - p_error("please use html-like colors (e.g. #ffbbaa)"); - - if ($angle !== 0) - $text_command = " q $rotate_command $text_command Q"; - + + if ($r !== null) { + $text_command = sprintf(' q %d %s %s rg %s Q', $r, $g, $b, $text_command); + } // Color RGB + } else { + throw new PDFException('please use html-like colors (e.g. #ffbbaa)'); + } + + if ($angle != 0) { + $text_command = sprintf(' q %s %s Q', $rotate_command, $text_command); + } + $data .= $text_command; $contents_obj->set_stream($data, false); // Update the contents $this->add_object($resources_obj); - $this->add_object($contents_obj); + $this->add_object($contents_obj); } /** * Adds an image to the document, in the specific page * NOTE: the image inclusion is taken from http://www.fpdf.org/; this is an adaptation - * and simplification of function Image(); it does not take care about units nor + * and simplification of function Image(); it does not take care about units nor * page breaks + * * @param page_obj the page object (or the page number) in which the image will appear * @param filename the name of the file that contains the image (or the content of the file, with the character '@' prepended) * @param x the x position (in pixels) where the image will appear @@ -168,35 +169,40 @@ public function add_text($page_to_appear, $text, $x, $y, $params = []) { * @param w the width of the image * @param w the height of the image */ - public function add_image($page_obj, $filename, $x=0, $y=0, $w=0, $h=0) { - + public function add_image($page_obj, $filename, $x = 0, $y = 0, $w = 0, $h = 0): bool + { // TODO: maybe we can create a function that "adds content to a page", and that // function will search for the content field and merge the resources, if // needed - p_warning("This function still needs work"); + $this->logger?->warning('This function still needs work'); // Check that the page is valid - if (is_int($page_obj)) + if (is_int($page_obj)) { $page_obj = $this->get_page($page_obj); + } + + if ($page_obj === false) { + throw new PDFException('invalid page'); + } - if ($page_obj === false) - return p_error("invalid page"); - // Get the page height, to change the coordinates system (up to down) $pagesize = $this->get_page_size($page_obj); - $pagesize_h = floatval("" . $pagesize[3]) - floatval("" . $pagesize[1]); + $pagesize_h = (float) ('' . $pagesize[3]) - (float) ('' . $pagesize[1]); - $result = $this->_add_image($filename, $x, $pagesize_h - $y, $w, $h); - - return p_error("this function still needs work"); + _add_image($filename, $x, $pagesize_h - $y, $w, $h); + throw new PDFException('this function still needs work'); // Get the resources for the page $resources_obj = $this->get_indirect_object($page_obj['Resources']); - if (!isset($resources_obj['ProcSet'])) + if (! isset($resources_obj['ProcSet'])) { $resources_obj['ProcSet'] = new PDFValueList(['/PDF']); + } + $resources_obj['ProcSet']->push(['/ImageB', '/ImageC', '/ImageI']); - if (!isset($resources_obj['XObject'])) + if (! isset($resources_obj['XObject'])) { $resources_obj['XObject'] = new PDFValueObject(); + } + $resources_obj['XObject'][$info['i']] = new PDFValueReference($images_objects[0]->get_oid()); // TODO: get the contents object in which to add the image. @@ -206,7 +212,7 @@ public function add_image($page_obj, $filename, $x=0, $y=0, $w=0, $h=0) { $data = $contents_obj->get_stream(false); if ($data === false) { - return p_error("could not interpret the contents of the page"); + throw new PDFException('could not interpret the contents of the page'); } // Append the command to draw the image @@ -219,14 +225,15 @@ public function add_image($page_obj, $filename, $x=0, $y=0, $w=0, $h=0) { $page_obj['Group'] = new PDFValueObject([ 'Type' => '/Group', 'S' => '/Transparency', - 'CS' => '/DeviceRGB' + 'CS' => '/DeviceRGB', ]); $this->add_object($page_obj); } - foreach ([$resources_obj, $contents_obj] as $o ) + foreach ([$resources_obj, $contents_obj] as $o) { $this->add_object($o); + } return true; } -} \ No newline at end of file +} diff --git a/src/PDFException.php b/src/PDFException.php new file mode 100644 index 0000000..d0d31bb --- /dev/null +++ b/src/PDFException.php @@ -0,0 +1,9 @@ + $v) { $obj[$field] = $v; } + $value = $obj; } - $this->_oid = $oid; $this->_value = $value; - $this->_generation = $generation; } - public function get_keys() { + public function __toString(): string + { + return $this->_oid . ' 0 obj +' . + $this->_value . PHP_EOL . + ( + $this->_stream === null ? '' : + "stream\n" . + '...' . + "\nendstream\n" + ) . + "endobj\n"; + } + + public function get_keys() + { return $this->_value->get_keys(); } - public function set_oid($oid) { + public function set_oid(int $oid): void + { $this->_oid = $oid; } - public function get_generation() { + public function get_generation(): int + { return $this->_generation; } - public function __toString() { - return "$this->_oid 0 obj\n" . - "$this->_value\n" . - ($this->_stream === null?"": - "stream\n" . - '...' . - "\nendstream\n" - ) . - "endobj\n"; - } /** * Converts the object to a well-formed PDF entry with a form like * 1 0 obj @@ -107,197 +110,228 @@ public function __toString() { * ... * endstream * endobj + * * @return pdfentry a string that contains the PDF entry */ - public function to_pdf_entry() { - return "$this->_oid 0 obj" . __EOL . - "$this->_value" . __EOL . - ($this->_stream === null?"": - "stream\r\n" . - $this->_stream . - __EOL . "endstream" . __EOL - ) . - "endobj" . __EOL; + public function to_pdf_entry(): string + { + return $this->_oid . ' 0 obj' . __EOL . + $this->_value . __EOL . + ( + $this->_stream === null ? '' : + "stream\r\n" . + $this->_stream . + __EOL . 'endstream' . __EOL + ) . + 'endobj' . __EOL; } + /** * Gets the object ID + * * @return oid the object id */ - public function get_oid() { + public function get_oid(): int + { return $this->_oid; } + /** * Gets the definition of the object (a PDFValue object) + * * @return value the definition of the object */ - public function get_value() { + public function get_value() + { return $this->_value; } - protected static function FlateDecode($_stream, $params) { - switch ($params["Predictor"]->get_int()) { - case 1: - return $_stream; - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - break; - default: - return p_error("other predictor than PNG is not supported in this version"); - } - - switch($params["Colors"]->get_int()) { - case 1: - break; - default: - return p_error("other color count than 1 is not supported in this version"); - } - - switch($params["BitsPerComponent"]->get_int()) { - case 8: - break; - default: - return p_error("other bit count than 8 is not supported in this version"); - } - - $decoded = new Buffer(); - $columns = $params['Columns']->get_int(); - - $row_len = $columns + 1; - $stream_len = strlen($_stream); - - // The previous row is zero - $data_prev = str_pad("", $columns, chr(0)); - $row_i = 0; - $pos_i = 0; - $data = str_pad("", $columns, chr(0)); - while ($pos_i < $stream_len) { - $filter_byte = ord($_stream[$pos_i++]); - - // Get the current row - $data = substr($_stream, $pos_i, $columns); - $pos_i += strlen($data); - - // Zero pad, in case that the content is not paired - $data = str_pad($data, $columns, chr(0)); - - // Depending on the filter byte of the row, we should unpack on one way or another - switch ($filter_byte) { - case 0: - break; - case 1: - for ($i = 1; $i < $columns; $i++) - $data[$i] = ($data[$i] + $data[$i-1]) % 256; - break; - case 2: - for ($i = 0; $i < $columns; $i++) { - $data[$i] = chr((ord($data[$i]) + ord($data_prev[$i])) % 256); - } - break; - default: - return p_error("Unsupported stream"); - } - - // Store and prepare the previous row - $decoded->data($data); - $data_prev = $data; - } - - // p_debug_var($decoded->show_bytes($columns)); - return $decoded->get_raw(); - } /** * Gets the stream of the object + * * @return stream a string that contains the stream of the object */ - public function get_stream($raw = true) { - if ($raw === true) + public function get_stream(bool $raw = true): Buffer|false + { + if ($raw) { return $this->_stream; + } + if (isset($this->_value['Filter'])) { switch ($this->_value['Filter']) { case '/FlateDecode': - $DecodeParams = $this->_value['DecodeParms']??[]; + $DecodeParams = $this->_value['DecodeParms'] ?? []; $params = [ - "Columns" => $DecodeParams['Columns']??new PDFValueSimple(0), - "Predictor" => $DecodeParams['Predictor']??new PDFValueSimple(1), - "BitsPerComponent" => $DecodeParams['BitsPerComponent']??new PDFValueSimple(8), - "Colors" => $DecodeParams['Colors']??new PDFValueSimple(1) + 'Columns' => $DecodeParams['Columns'] ?? new PDFValueSimple(0), + 'Predictor' => $DecodeParams['Predictor'] ?? new PDFValueSimple(1), + 'BitsPerComponent' => $DecodeParams['BitsPerComponent'] ?? new PDFValueSimple(8), + 'Colors' => $DecodeParams['Colors'] ?? new PDFValueSimple(1), ]; + return self::FlateDecode(gzuncompress($this->_stream), $params); - - break; default: - return p_error('unknown compression method ' . $this->_value['Filter']); + throw new PDFException('unknown compression method ' . $this->_value['Filter']); } } + return $this->_stream; } + /** * Sets the stream for the object (overwrites a previous existing stream) + * * @param stream the stream for the object */ - public function set_stream($stream, $raw = true) { - if ($raw === true) { + public function set_stream($stream, bool $raw = true): void + { + if ($raw) { $this->_stream = $stream; + return; } + if (isset($this->_value['Filter'])) { - switch ($this->_value['Filter']) { - case '/FlateDecode': - $stream = gzcompress($stream); - break; - default: - p_error('unknown compression method ' . $this->_value['Filter']); - } + $stream = match ($this->_value['Filter']) { + '/FlateDecode' => gzcompress((string) $stream), + default => throw new PDFException('unknown compression method ' . $this->_value['Filter']), + }; } - $this->_value['Length'] = strlen($stream); + + $this->_value['Length'] = strlen((string) $stream); $this->_stream = $stream; - } + } + /** * The next functions enble to make use of this object in an array-like manner, * using the name of the fields as positions in the array. It is useful is the * value is of type PDFValueObject or PDFValueList, using indexes */ - - /** + /** * Sets the value of the field offset, using notation $obj['field'] = $value + * * @param field the field to set the value * @param value the value to set - * @return void */ - public function offsetSet($field, $value) : void { - $this->_value[$field] = $value; + public function offsetSet(mixed $offset, mixed $value): void + { + $this->_value[$offset] = $value; } + /** * Checks whether the field exists in the object or not (or if the index exists * in the list) + * * @param field the field to check wether exists or not + * * @return exists true if the field exists; false otherwise */ - public function offsetExists ( $field ) : bool { - return $this->_value->offsetExists($field); + public function offsetExists(mixed $offset): bool + { + return $this->_value->offsetExists($offset); } + /** * Gets the value of the field (or the value at position) + * * @param field the field to get the value + * * @return value the value of the field */ - #[\ReturnTypeWillChange] - public function offsetGet ( $field ) { - return $this->_value[$field]; + #[ReturnTypeWillChange] + public function offsetGet(mixed $offset) + { + return $this->_value[$offset]; } + /** * Unsets the value of the field (or the value at position) + * * @param field the field to unset the value */ - public function offsetUnset($field ) : void { - $this->_value->offsetUnset($field); - } + public function offsetUnset(mixed $offset): void + { + $this->_value->offsetUnset($offset); + } - public function push($v) { + public function push(mixed $v): bool + { return $this->_value->push($v); } + + protected static function FlateDecode($_stream, array $params): Buffer|string|null + { + switch ($params['Predictor']->get_int()) { + case 1: + return $_stream; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + break; + default: + throw new PDFException('other predictor than PNG is not supported in this version'); + } + + switch ($params['Colors']->get_int()) { + case 1: + break; + default: + throw new PDFException('other color count than 1 is not supported in this version'); + } + + switch ($params['BitsPerComponent']->get_int()) { + case 8: + break; + default: + throw new PDFException('other bit count than 8 is not supported in this version'); + } + + $decoded = new Buffer(); + $columns = $params['Columns']->get_int(); + + $stream_len = strlen((string) $_stream); + + // The previous row is zero + $data_prev = str_pad('', $columns, chr(0)); + $pos_i = 0; + while ($pos_i < $stream_len) { + $filter_byte = ord($_stream[$pos_i++]); + + // Get the current row + $data = substr((string) $_stream, $pos_i, $columns); + $pos_i += strlen($data); + + // Zero pad, in case that the content is not paired + $data = str_pad($data, $columns, chr(0)); + + // Depending on the filter byte of the row, we should unpack on one way or another + switch ($filter_byte) { + case 0: + break; + case 1: + for ($i = 1; $i < $columns; $i++) { + $data[$i] = ($data[$i] + $data[$i - 1]) % 256; + } + + break; + case 2: + for ($i = 0; $i < $columns; $i++) { + $data[$i] = chr((ord($data[$i]) + ord($data_prev[$i])) % 256); + } + + break; + default: + throw new PDFException('Unsupported stream'); + } + + // Store and prepare the previous row + $decoded->data($data); + $data_prev = $data; + } + + // p_debug_var($decoded->show_bytes($columns)); + return $decoded->get_raw(); + } } diff --git a/src/PDFObjectParser.php b/src/PDFObjectParser.php index 0158ad9..e40eea6 100644 --- a/src/PDFObjectParser.php +++ b/src/PDFObjectParser.php @@ -19,432 +19,480 @@ along with this program. If not, see . */ - namespace ddn\sapp; - - use ddn\sapp\pdfvalue\PDFValue; - use ddn\sapp\pdfvalue\PDFValueHexString; - use ddn\sapp\pdfvalue\PDFValueList; - use ddn\sapp\pdfvalue\PDFValueObject; - use ddn\sapp\pdfvalue\PDFValueReference; - use ddn\sapp\pdfvalue\PDFValueSimple; - use ddn\sapp\pdfvalue\PDFValueString; - use ddn\sapp\pdfvalue\PDFValueType; - use ddn\sapp\helpers\StreamReader; - use \Exception; - - use ddn\sapp\helpers\Buffer; - - use function ddn\sapp\helpers\p_debug; - use function ddn\sapp\helpers\p_debug_var; - use function ddn\sapp\helpers\p_error; - use function ddn\sapp\helpers\p_warning; - +namespace ddn\sapp; + +use ddn\sapp\helpers\StreamReader; +use ddn\sapp\pdfvalue\PDFValue; +use ddn\sapp\pdfvalue\PDFValueHexString; +use ddn\sapp\pdfvalue\PDFValueList; +use ddn\sapp\pdfvalue\PDFValueObject; +use ddn\sapp\pdfvalue\PDFValueSimple; +use ddn\sapp\pdfvalue\PDFValueString; +use ddn\sapp\pdfvalue\PDFValueType; +use Exception; +use Stringable; + +/** + * Class devoted to parse a single PDF object + * A PDF Document is made of objects with the following structure (e.g for object 1 version 0) + * 1 0 obj + * ...content... + * [stream + * ...stream... + * endstream] + * endobject + * This PDF class transforms the definition string within ...content... into a PDFValue class. + * - At the end, it is a simple syntax checker + */ +class PDFObjectParser implements Stringable +{ + // Possible tokens in a PDF document + public const T_NOTOKEN = 0; + + public const T_LIST_START = 1; + + public const T_LIST_END = 2; + + public const T_FIELD = 3; + + public const T_STRING = 4; + + public const T_HEX_STRING = 12; + + public const T_SIMPLE = 5; + + public const T_DICT_START = 6; + + public const T_DICT_END = 7; + + public const T_OBJECT_BEGIN = 8; + + public const T_OBJECT_END = 9; + + public const T_STREAM_BEGIN = 10; + + public const T_STREAM_END = 11; + + public const T_COMMENT = 13; + + public const T_NAMES = [ + self::T_NOTOKEN => 'no token', + self::T_LIST_START => 'list start', + self::T_LIST_END => 'list end', + self::T_FIELD => 'field', + self::T_STRING => 'string', + self::T_HEX_STRING => 'hex string', + self::T_SIMPLE => 'simple', + self::T_DICT_START => 'dict start', + self::T_DICT_END => 'dict end', + self::T_OBJECT_BEGIN => 'object begin', + self::T_OBJECT_END => 'object end', + self::T_STREAM_BEGIN => 'stream begin', + self::T_STREAM_END => 'stream end', + self::T_COMMENT => 'comment', + ]; + + public const T_SIMPLE_OBJECTS = [ + self::T_SIMPLE, + self::T_OBJECT_BEGIN, + self::T_OBJECT_END, + self::T_STREAM_BEGIN, + self::T_STREAM_END, + self::T_COMMENT, + ]; + + protected $_buffer; + + protected $_c = false; + + protected $_n = false; + + protected $_t = false; + + protected int $_tt = self::T_NOTOKEN; /** - * Class devoted to parse a single PDF object - * - * A PDF Document is made of objects with the following structure (e.g for object 1 version 0) - * - * 1 0 obj - * ...content... - * [stream - * ...stream... - * endstream] - * endobject - * - * This PDF class transforms the definition string within ...content... into a PDFValue class. - * - * - At the end, it is a simple syntax checker + * Simple output of the object + * + * @return output the output of the object */ - class PDFObjectParser { - - // Possible tokens in a PDF document - const T_NOTOKEN = 0; - const T_LIST_START = 1; - const T_LIST_END = 2; - const T_FIELD = 3; - const T_STRING = 4; - const T_HEX_STRING = 12; - const T_SIMPLE = 5; - const T_DICT_START = 6; - const T_DICT_END = 7; - const T_OBJECT_BEGIN = 8; - const T_OBJECT_END = 9; - const T_STREAM_BEGIN = 10; - const T_STREAM_END = 11; - const T_COMMENT = 13; - - const T_NAMES = [ - self::T_NOTOKEN => 'no token', - self::T_LIST_START => 'list start', - self::T_LIST_END => 'list end', - self::T_FIELD => 'field', - self::T_STRING => 'string', - self::T_HEX_STRING => 'hex string', - self::T_SIMPLE => 'simple', - self::T_DICT_START => 'dict start', - self::T_DICT_END => 'dict end', - self::T_OBJECT_BEGIN => 'object begin', - self::T_OBJECT_END => 'object end', - self::T_STREAM_BEGIN => 'stream begin', - self::T_STREAM_END => 'stream end', - self::T_COMMENT => 'comment' - ]; - - const T_SIMPLE_OBJECTS = [ - self::T_SIMPLE, - self::T_OBJECT_BEGIN, - self::T_OBJECT_END, - self::T_STREAM_BEGIN, - self::T_STREAM_END, - self::T_COMMENT - ]; - - protected $_buffer = null; - protected $_c = false; - protected $_n = false; - protected $_t = false; - protected $_tt = self::T_NOTOKEN; - - /** - * Retrieves the current token type (one of T_* constants) - * @return token the current token - */ - public function current_token() { - return $this->_tt; - } + public function __toString(): string + { + return 'pos: ' . $this->_buffer->getpos() . sprintf(', c: %s, n: %s, t: %s, tt: ', $this->_c, $this->_n, $this->_t) . + self::T_NAMES[$this->_tt] . ', b: ' . $this->_buffer->substratpos(50) . + "\n"; + } - /** - * Obtains the next char and prepares the variable $this->_c and $this->_n to contain the current char and the next char - * - if EOF, _c will be false - * - if the last char before EOF, _n will be false - * @return char the next char - */ - protected function nextchar() { - $this->_c = $this->_n; - $this->_n = $this->_buffer->nextchar(); - return $this->_c; - } + /** + * Retrieves the current token type (one of T_* constants) + * + * @return int the current token + */ + public function current_token(): int + { + return $this->_tt; + } - /** - * Prepares the parser to analythe the text (i.e. prepares the parsing variables) - */ - protected function start(&$buffer) { - $this->_buffer = $buffer; - $this->_c = false; - $this->_n = false; - $this->_t = false; - $this->_tt = self::T_NOTOKEN; - - if ($this->_buffer->size() === 0) return false; - $this->_n = $this->_buffer->currentchar(); - $this->nextchar(); - } + /** + * Parses the document + */ + public function parse(StreamReader $stream): PDFValue|false|null + { // $str, $offset = 0) { + $this->start($stream); //$str, $offset); + $this->nexttoken(); - /** - * Parses the document - */ - public function parse(&$stream) { // $str, $offset = 0) { - $this->start($stream); //$str, $offset); - $this->nexttoken(); - $result = $this->_parse_value(); - return $result; - } + return $this->_parse_value(); + } + + public function parsestr(string $str, int $offset = 0): PDFValue|false|null + { + $stream = new StreamReader($str); + $stream->goto($offset); + + return $this->parse($stream); + } + + /** + * Obtains the next token and returns it + */ + public function nexttoken() + { + [$this->_t, $this->_tt] = $this->token(); + + return $this->_t; + } + + /** + * Obtains the next char and prepares the variable $this->_c and $this->_n to contain the current char and the next char + * - if EOF, _c will be false + * - if the last char before EOF, _n will be false + * + * @return char the next char + */ + protected function nextchar() + { + $this->_c = $this->_n; + $this->_n = $this->_buffer->nextchar(); - public function parsestr($str, $offset = 0) { - $stream = new StreamReader($str); - $stream->goto($offset); - return $this->parse($stream); + return $this->_c; + } + + /** + * Prepares the parser to analythe the text (i.e. prepares the parsing variables) + */ + protected function start(StreamReader $buffer): bool|null + { + $this->_buffer = $buffer; + $this->_c = false; + $this->_n = false; + $this->_t = false; + $this->_tt = self::T_NOTOKEN; + + if ($this->_buffer->size() === 0) { + return false; } - /** - * Simple output of the object - * @return output the output of the object - */ - public function __toString() { - return "pos: " . $this->_buffer->getpos() . ", c: $this->_c, n: $this->_n, t: $this->_t, tt: " . - self::T_NAMES[$this->_tt] . ', b: ' . $this->_buffer->substratpos(50) . - "\n"; + $this->_n = $this->_buffer->currentchar(); + $this->nextchar(); + + return false; + } + + /** + * Function that returns wether the current char is a separator or not + */ + protected function _c_is_separator(): bool + { + $DSEPS = ['<<', '>>']; + + return $this->_c === false || str_contains("%<>()[]{}/ \n\r\t", (string) $this->_c) || in_array($this->_c . $this->_n, $DSEPS, true); + } + + /** + * This function assumes that the next content is an hex string, so it should be called after "<" is detected; it skips the trailing ">" + * + * @return string the hex string + */ + protected function _parse_hex_string() + { + $token = ''; + + if ($this->_c !== '<') { + throw new Exception('Invalid hex string'); } - /** - * Obtains the next token and returns it - */ - public function nexttoken() { - [ $this->_t, $this->_tt ] = $this->token(); - return $this->_t; + $this->nextchar(); // This char is "<" + while ($this->_c !== '>' && str_contains("0123456789abcdefABCDEF \t\r\n\f", $this->_c)) { + $token .= $this->_c; + if ($this->nextchar() === false) { + break; + } } - /** - * Function that returns wether the current char is a separator or not - */ - protected function _c_is_separator() { - $DSEPS =[ "<<", ">>" ]; + if ($this->_c !== false && ! str_contains(">0123456789abcdefABCDEF \t\r\n\f", $this->_c)) { + throw new Exception('invalid hex string'); + } - return (($this->_c === false) || (strpos("%<>()[]{}/ \n\r\t", $this->_c) !== false) || ((array_search($this->_c . $this->_n, $DSEPS)) !== false)); + // The only way to get to here is that char is ">" + if ($this->_c !== '>') { + throw new Exception('Invalid hex string'); } - /** - * This function assumes that the next content is an hex string, so it should be called after "<" is detected; it skips the trailing ">" - * @return string the hex string - */ - protected function _parse_hex_string() { - $token = ""; + $this->nextchar(); - if ($this->_c !== "<") throw new Exception("Invalid hex string"); + return $token; + } - $this->nextchar(); // This char is "<" - while (($this->_c !== '>')&&(strpos("0123456789abcdefABCDEF \t\r\n\f", $this->_c) !== false)) { - $token .= $this->_c; - if ($this->nextchar() === false) { + protected function _parse_string() + { + $token = ''; + if ($this->_c !== '(') { + throw new Exception('Invalid string'); + } + + $n_parenthesis = 1; + while ($this->_c !== false) { + $this->nextchar(); + if ($this->_c === ')' && (! strlen($token) || $token[strlen($token) - 1] !== '\\')) { + $n_parenthesis--; + if ($n_parenthesis === 0) { break; } - } - if (($this->_c !== false) && (strpos(">0123456789abcdefABCDEF \t\r\n\f", $this->_c) === false)) - throw new Exception("invalid hex string"); - - // The only way to get to here is that char is ">" - if ($this->_c !== ">") throw new Exception("Invalid hex string"); + } else { + if ($this->_c === '(' && (! strlen($token) || $token[strlen($token) - 1] !== '\\')) { + $n_parenthesis++; + } - $this->nextchar(); - return $token; + $token .= $this->_c; + } } - protected function _parse_string() { - $token = ""; - if ($this->_c !== "(") throw new Exception("Invalid string"); + if ($this->_c !== ')') { + throw new Exception('Invalid string'); + } - $n_parenthesis = 1; - while ($this->_c !== false) { - $this->nextchar(); - if (($this->_c === ')') && (!strlen($token) || ($token[strlen($token) - 1] !== '\\'))) { - $n_parenthesis--; - if ($n_parenthesis == 0) - break; - } else { - if (($this->_c === '(') && (!strlen($token) || ($token[strlen($token) - 1] !== '\\'))) { - $n_parenthesis++; - } - $token .= $this->_c; - } - } + $this->nextchar(); - if ($this->_c !== ")") { - throw new Exception("Invalid string"); - } - $this->nextchar(); + return $token; + } - return $token; + protected function token(): array + { + if ($this->_c === false) { + return [false, 0]; } - protected function token() { - if ($this->_c === false) return [ false, false ]; - - $token = false; + $token = false; - while ($this->_c !== false) { - // Skip the spaces - while ((strpos("\t\n\r ", $this->_c) !== false) && ($this->nextchar() !== false)) ; + while ($this->_c !== false) { + // Skip the spaces + while (str_contains("\t\n\r ", (string) $this->_c) && $this->nextchar() !== false) { + } - $token_type = self::T_NOTOKEN; + $token_type = self::T_NOTOKEN; - // TODO: also the special characters are not "strictly" considered, according to section 7.3.4.2: \n \r \t \b \f \( \) \\ are valid; the other not; but also \bbb should be considered; all of them are "sufficiently" treated, but other unknown caracters such as \u are also accepted - switch ($this->_c) { - case '%': + // TODO: also the special characters are not "strictly" considered, according to section 7.3.4.2: \n \r \t \b \f \( \) \\ are valid; the other not; but also \bbb should be considered; all of them are "sufficiently" treated, but other unknown caracters such as \u are also accepted + switch ($this->_c) { + case '%': + $this->nextchar(); + $token = ''; + while (! str_contains("\n\r", (string) $this->_c)) { + $token .= $this->_c; $this->nextchar(); - $token = ""; - while (strpos("\n\r", $this->_c) === false) { - $token .= $this->_c; - $this->nextchar(); - } - $token_type = self::T_COMMENT; - break; - case '<': - if ($this->_n === '<') { - $this->nextchar(); - $this->nextchar(); - $token = '<<'; - $token_type = self::T_DICT_START; - } else { - $token = $this->_parse_hex_string(); - $token_type = self::T_HEX_STRING; - } - break; - case '(': - $token = $this->_parse_string(); - $token_type = self::T_STRING; - break; - case '>': - if ($this->_n === '>') { - $this->nextchar(); - $this->nextchar(); - $token = '>>'; - $token_type = self::T_DICT_END; - } - break; - case '[': - $token = $this->_c; + } + + $token_type = self::T_COMMENT; + break; + case '<': + if ($this->_n === '<') { $this->nextchar(); - $token_type = self::T_LIST_START; - break; - case ']': - $token = $this->_c; $this->nextchar(); - $token_type = self::T_LIST_END; - break; - case '/': - // Skip the field idenifyer + $token = '<<'; + $token_type = self::T_DICT_START; + } else { + $token = $this->_parse_hex_string(); + $token_type = self::T_HEX_STRING; + } + + break; + case '(': + $token = $this->_parse_string(); + $token_type = self::T_STRING; + break; + case '>': + if ($this->_n === '>') { + $this->nextchar(); $this->nextchar(); + $token = '>>'; + $token_type = self::T_DICT_END; + } - // We are assuming any char (i.e. /MY+difficult_id is valid) - while (!$this->_c_is_separator()) { - $token .= $this->_c; - if ($this->nextchar() === false) break; - } - $token_type = self::T_FIELD; - break; - } - if ($token === false) { - $token = ""; + break; + case '[': + $token = $this->_c; + $this->nextchar(); + $token_type = self::T_LIST_START; + break; + case ']': + $token = $this->_c; + $this->nextchar(); + $token_type = self::T_LIST_END; + break; + case '/': + // Skip the field idenifyer + $this->nextchar(); - while (!$this->_c_is_separator()) { + // We are assuming any char (i.e. /MY+difficult_id is valid) + while (! $this->_c_is_separator()) { $token .= $this->_c; - if ($this->nextchar() === false) break; + if ($this->nextchar() === false) { + break; + } } - switch ($token) { - case 'obj': - $token_type = self::T_OBJECT_BEGIN; break; - case 'endobj': - $token_type = self::T_OBJECT_END; break; - case 'stream': - $token_type = self::T_STREAM_BEGIN; break; - case 'endstream': - $token_type = self::T_STREAM_END; break; - default: - $token_type = self::T_SIMPLE; break; - } - - } - return [ $token, $token_type ]; + $token_type = self::T_FIELD; + break; } - } - protected function _parse_obj() { - if ($this->_tt !== self::T_DICT_START) { - throw new Exception("Invalid object definition"); - } + if ($token === false) { + $token = ''; - $this->nexttoken(); - $object = []; - while ($this->_t !== false) { - switch ($this->_tt) { - case self::T_FIELD: - $field = $this->_t; - $this->nexttoken(); - $object[$field] = $this->_parse_value(); - break; - case self::T_DICT_END: - $this->nexttoken(); - return new PDFValueObject($object); + while (! $this->_c_is_separator()) { + $token .= $this->_c; + if ($this->nextchar() === false) { break; - default: - throw new Exception("Invalid token: $this"); + } } + + $token_type = match ($token) { + 'obj' => self::T_OBJECT_BEGIN, + 'endobj' => self::T_OBJECT_END, + 'stream' => self::T_STREAM_BEGIN, + 'endstream' => self::T_STREAM_END, + default => self::T_SIMPLE, + }; } - return false; + + return [$token, $token_type]; } - protected function _parse_list() { - if ($this->_tt !== self::T_LIST_START) { - throw new Exception("Invalid list definition"); - } + return [false, 0]; + } - $this->nexttoken(); - $list = []; - while ($this->_t !== false) { - switch ($this->_tt) { - case self::T_LIST_END: - $this->nexttoken(); - return new PDFValueList($list); + protected function _parse_obj(): PDFValueObject|false + { + if ($this->_tt !== self::T_DICT_START) { + throw new Exception('Invalid object definition'); + } - case self::T_OBJECT_BEGIN: - case self::T_OBJECT_END: - case self::T_STREAM_BEGIN: - case self::T_STREAM_END: - throw new Exception("Invalid list definition"); - break; - default: - $value = $this->_parse_value(); - if ($value !== false) { - array_push($list, $value); - } - break; - } + $this->nexttoken(); + $object = []; + while ($this->_t !== false) { + switch ($this->_tt) { + case self::T_FIELD: + $field = $this->_t; + $this->nexttoken(); + $object[$field] = $this->_parse_value(); + break; + case self::T_DICT_END: + $this->nexttoken(); + + return new PDFValueObject($object); + default: + throw new Exception('Invalid token: ' . $this); } - return new PDFValueList($list); } - protected function _parse_value() { - while ($this->_t !== false) { - switch ($this->_tt) { - case self::T_DICT_START: - return $this->_parse_obj(); - break; - case self::T_LIST_START: - return $this->_parse_list(); - break; - case self::T_STRING: - $string = new PDFValueString($this->_t); - $this->nexttoken(); - return $string; - break; - case self::T_HEX_STRING: - $string = new PDFValueHexString($this->_t); - $this->nexttoken(); - return $string; - break; - case self::T_FIELD: - $field = new PDFValueType($this->_t); - $this->nexttoken(); - return $field; - case self::T_OBJECT_BEGIN: - case self::T_STREAM_END: - throw new Exception("invalid keyword"); - - case self::T_OBJECT_END: - case self::T_STREAM_BEGIN: - return null; - case self::T_COMMENT: - $this->nexttoken(); - break; - case self::T_SIMPLE: - $simple_value = $this->_t; - $this->nexttoken(); + return false; + } - while (($this->_t !== false) && ($this->_tt == self::T_SIMPLE)) { - $simple_value .= " " . $this->_t; - $this->nexttoken(); - } - /* - WON'T DO IT: the meaning of each element in a list is contextual; e.g. [ 10 10 0 R ] may mean [ 10 "reference to object 10" ], or [ 10 10 0 R ], where R is - a value for a flag... as SAPP is agnostic, we won't break it into objects; instead we offer the "get_referenced_object" for "simple" and "list" objects - and then the app that uses SAPP will decide wether it may use the values as object references or not. - */ - return new PDFValueSimple($simple_value); - break; - - default: - throw new Exception("Invalid token: $this"); - } + protected function _parse_list(): PDFValueList + { + if ($this->_tt !== self::T_LIST_START) { + throw new Exception('Invalid list definition'); + } + + $this->nexttoken(); + $list = []; + while ($this->_t !== false) { + switch ($this->_tt) { + case self::T_LIST_END: + $this->nexttoken(); + + return new PDFValueList($list); + + case self::T_OBJECT_BEGIN: + case self::T_OBJECT_END: + case self::T_STREAM_BEGIN: + case self::T_STREAM_END: + throw new Exception('Invalid list definition'); + default: + $value = $this->_parse_value(); + if ($value !== false) { + $list[] = $value; + } + + break; } - return false; } - function tokenize() { - $this->start(); - while ($this->nexttoken() !== false) { - echo "$this->_t\n"; + return new PDFValueList($list); + } + + protected function _parse_value(): PDFValue|false|null + { + while ($this->_t !== false) { + switch ($this->_tt) { + case self::T_DICT_START: + return $this->_parse_obj(); + case self::T_LIST_START: + return $this->_parse_list(); + case self::T_STRING: + $string = new PDFValueString($this->_t); + $this->nexttoken(); + + return $string; + case self::T_HEX_STRING: + $string = new PDFValueHexString($this->_t); + $this->nexttoken(); + + return $string; + case self::T_FIELD: + $field = new PDFValueType($this->_t); + $this->nexttoken(); + + return $field; + case self::T_OBJECT_BEGIN: + case self::T_STREAM_END: + throw new Exception('invalid keyword'); + case self::T_OBJECT_END: + case self::T_STREAM_BEGIN: + return null; + case self::T_COMMENT: + $this->nexttoken(); + break; + case self::T_SIMPLE: + $simple_value = $this->_t; + $this->nexttoken(); + + while ($this->_t !== false && $this->_tt === self::T_SIMPLE) { + $simple_value .= ' ' . $this->_t; + $this->nexttoken(); + } + + /* + WON'T DO IT: the meaning of each element in a list is contextual; e.g. [ 10 10 0 R ] may mean [ 10 "reference to object 10" ], or [ 10 10 0 R ], where R is + a value for a flag... as SAPP is agnostic, we won't break it into objects; instead we offer the "get_referenced_object" for "simple" and "list" objects + and then the app that uses SAPP will decide wether it may use the values as object references or not. + */ + + return new PDFValueSimple($simple_value); + + default: + throw new Exception('Invalid token: ' . $this); } } + + return false; } +} diff --git a/src/PDFSignatureObject.php b/src/PDFSignatureObject.php index c9794da..1eefdc0 100644 --- a/src/PDFSignatureObject.php +++ b/src/PDFSignatureObject.php @@ -21,19 +21,13 @@ namespace ddn\sapp; -use ddn\sapp\PDFObject; -use ddn\sapp\pdfvalue\PDFValue; -use ddn\sapp\pdfvalue\PDFValueHexString; -use ddn\sapp\pdfvalue\PDFValueList; -use ddn\sapp\pdfvalue\PDFValueObject; -use ddn\sapp\pdfvalue\PDFValueReference; use ddn\sapp\pdfvalue\PDFValueSimple; use ddn\sapp\pdfvalue\PDFValueString; -use ddn\sapp\pdfvalue\PDFValueType; use function ddn\sapp\helpers\timestamp_to_pdfdatestring; // This is an special object that has a set of fields -class PDFSignatureObject extends PDFObject { +class PDFSignatureObject extends PDFObject +{ // The maximum signature length, needed to create a placeholder to calculate the range of bytes // that will cover the signature. public static $__SIGNATURE_MAX_LENGTH = 27742; @@ -42,117 +36,151 @@ class PDFSignatureObject extends PDFObject { // is not known. 68 digits enable 20 digits for the size of the document public static $__BYTERANGE_SIZE = 68; - protected $_prev_content_size = 0; - protected $_post_content_size = null; + protected int $_prev_content_size = 0; + + protected $_post_content_size; // A placeholder for the certificate to use to sign the document - protected $_certificate = null; - protected $_signature_ltv_data = null; - protected $_signature_tsa = null; + protected $_certificate; + + protected $_signature_ltv_data; + + protected $_signature_tsa; + + /** + * Constructs the object and sets the default values needed to sign + * + * @param oid the oid for the object + */ + public function __construct(int $oid) + { + parent::__construct($oid, [ + 'Filter' => '/Adobe.PPKLite', + 'Type' => '/Sig', + 'SubFilter' => '/adbe.pkcs7.detached', + 'ByteRange' => new PDFValueSimple(str_repeat(' ', self::$__BYTERANGE_SIZE)), + 'Contents' => '<' . str_repeat('0', self::$__SIGNATURE_MAX_LENGTH) . '>', + 'M' => new PDFValueString(timestamp_to_pdfdatestring()), + ]); + } + /** * Sets the certificate to use to sign + * * @param cert the pem-formatted certificate and private to use to sign as * [ 'cert' => ..., 'pkey' => ... ] */ - public function set_certificate($certificate) { + public function set_certificate($certificate): void + { $this->_certificate = $certificate; } - public function set_signature_ltv($signature_ltv_data) { - $this->_signature_ltv_data = $signature_ltv_data; + + public function set_signature_ltv($signature_ltv_data): void + { + throw new PDFException(get_debug_type($signature_ltv_data)); } - public function set_signature_tsa($signature_tsa) { + + public function set_signature_tsa($signature_tsa): void + { $this->_signature_tsa = $signature_tsa; } + /** * Obtains the certificate set with function set_certificate + * * @return cert the certificate */ - public function get_certificate() { + public function get_certificate(): array + { return $this->_certificate; } - public function get_tsa() { + + public function get_tsa() + { return $this->_signature_tsa; } - public function get_ltv() { + + public function get_ltv() + { return $this->_signature_ltv_data; } - /** - * Constructs the object and sets the default values needed to sign - * @param oid the oid for the object - */ - public function __construct($oid) { - $this->_prev_content_size = 0; - $this->_post_content_size = null; - parent::__construct($oid, [ - 'Filter' => "/Adobe.PPKLite", - 'Type' => "/Sig", - 'SubFilter' => "/adbe.pkcs7.detached", - 'ByteRange' => new PDFValueSimple(str_repeat(" ", self::$__BYTERANGE_SIZE)), - 'Contents' => "<" . str_repeat("0", self::$__SIGNATURE_MAX_LENGTH) . ">", - 'M' => new PDFValueString(timestamp_to_pdfdatestring()), - ]); - } + /** * Function used to add some metadata fields to the signature: name, reason of signature, etc. + * * @param name the name of the signer * @param reason the reason for the signature * @param location the location of signature * @param contact the contact info */ - public function set_metadata($name = null, $reason = null, $location = null, $contact = null) { + public function set_metadata($name = null, $reason = null, $location = null, $contact = null): void + { if ($name !== null) { - $this->_value["Name"] = new PDFValueString($name); + $this->_value['Name'] = new PDFValueString($name); } + if ($reason !== null) { - $this->_value["Reason"] = new PDFValueString($reason); + $this->_value['Reason'] = new PDFValueString($reason); } + if ($location !== null) { - $this->_value["Location"] = new PDFValueString($location); + $this->_value['Location'] = new PDFValueString($location); } + if ($contact !== null) { - $this->_value["ContactInfo"] = new PDFValueString($contact); + $this->_value['ContactInfo'] = new PDFValueString($contact); } } + /** * Function that sets the size of the content that will appear in the file, previous to this object, * and the content that will be included after. This is needed to get the range of bytes of the * signature. */ - public function set_sizes($prev_content_size, $post_content_size = null) { + public function set_sizes(int $prev_content_size, $post_content_size = null): void + { $this->_prev_content_size = $prev_content_size; $this->_post_content_size = $post_content_size; } + /** * This function gets the offset of the marker, relative to this object. To make correct, the offset of the object * shall have properly been set. It makes use of the parent "to_pdf_entry" function to avoid recursivity. + * * @return position the position of the <0000 marker */ - public function get_signature_marker_offset() { + public function get_signature_marker_offset(): int + { $tmp_output = parent::to_pdf_entry(); - $marker = "/Contents"; + $marker = '/Contents'; $position = strpos($tmp_output, $marker); + return $position + strlen($marker); } + /** * Overrides the parent function to calculate the proper range of bytes, according to the sizes provided and the * string representation of this object + * * @return str the string representation of this object */ - public function to_pdf_entry() { + public function to_pdf_entry(): string + { $signature_size = strlen(parent::to_pdf_entry()); $offset = $this->get_signature_marker_offset(); $starting_second_part = $this->_prev_content_size + $offset + self::$__SIGNATURE_MAX_LENGTH + 2; - $contents_size = strlen("" . $this->_value['Contents']); + $contents_size = strlen('' . $this->_value['Contents']); - $byterange_str = "[ 0 " . - ($this->_prev_content_size + $offset) . " " . - ($starting_second_part) . " " . - ($this->_post_content_size!==null?$this->_post_content_size + ($signature_size - $contents_size - $offset):0) . " ]"; + $byterange_str = '[ 0 ' . + $this->_prev_content_size + $offset . ' ' . + $starting_second_part . ' ' . + ($this->_post_content_size !== null ? $this->_post_content_size + ($signature_size - $contents_size - $offset) : 0) . ' ]'; $this->_value['ByteRange'] = - new PDFValueSimple($byterange_str . str_repeat(" ", self::$__BYTERANGE_SIZE - strlen($byterange_str) + 1) - ); + new PDFValueSimple( + $byterange_str . str_repeat(' ', self::$__BYTERANGE_SIZE - strlen($byterange_str) + 1) + ); return parent::to_pdf_entry(); } diff --git a/src/PDFUtilFnc.php b/src/PDFUtilFnc.php index 083247b..8ab5033 100644 --- a/src/PDFUtilFnc.php +++ b/src/PDFUtilFnc.php @@ -21,28 +21,27 @@ namespace ddn\sapp; -use ddn\sapp\PDFObjectParser; -use ddn\sapp\helpers\StreamReader; use ddn\sapp\helpers\Buffer; -use function ddn\sapp\helpers\p_debug; -use function ddn\sapp\helpers\p_debug_var; -use function ddn\sapp\helpers\p_error; -use function ddn\sapp\helpers\p_warning; -use function ddn\sapp\helpers\show_bytes; - use ddn\sapp\helpers\LoadHelpers; -if (!defined("ddn\\sapp\\helpers\\LoadHelpers")) - new LoadHelpers; +use ddn\sapp\helpers\StreamReader; +use ddn\sapp\pdfvalue\PDFValue; +use Exception; -// TODO: use the streamreader to deal with the document in the file, instead of a buffer +if (! defined(LoadHelpers::class)) { + new LoadHelpers(); +} -class PDFUtilFnc { +// TODO: use the streamreader to deal with the document in the file, instead of a buffer - public static function get_trailer(&$_buffer, $trailer_pos) { +class PDFUtilFnc +{ + public static function get_trailer($_buffer, int $trailer_pos): PDFValue|false|null + { // Search for the trailer structure - if (preg_match('/trailer\s*(.*)\s*startxref/ms', $_buffer, $matches, 0, $trailer_pos) !== 1) - return p_error("trailer not found"); - + if (preg_match('/trailer\s*(.*)\s*startxref/ms', (string) $_buffer, $matches, 0, $trailer_pos) !== 1) { + throw new PDFException('trailer not found'); + } + $trailer_str = $matches[1]; // We create the object to parse (this is not innefficient, because it is disposed when returning from the function) @@ -50,15 +49,19 @@ public static function get_trailer(&$_buffer, $trailer_pos) { $parser = new PDFObjectParser(); try { $trailer_obj = $parser->parsestr($trailer_str); - } catch (Exception $e) { - return p_error("trailer is not valid"); + } catch (Exception $exception) { + throw new PDFException('trailer is not valid', 0, $exception); } return $trailer_obj; } - public static function build_xref_1_5($offsets) { - if (isset($offsets[0])) unset($offsets[0]); + public static function build_xref_1_5($offsets): array + { + if (isset($offsets[0])) { + unset($offsets[0]); + } + $k = array_keys($offsets); sort($k); @@ -66,48 +69,51 @@ public static function build_xref_1_5($offsets) { $i_k = 0; $c_k = 0; $count = 1; - $result = ""; - for ($i = 0; $i < count($k); $i++) { + $result = ''; + for ($i = 0, $iMax = count($k); $i < $iMax; $i++) { if ($c_k === 0) { $c_k = $k[$i] - 1; $i_k = $k[$i]; $count = 0; } + if ($k[$i] === $c_k + 1) { $count++; } else { - array_push($indexes, "$i_k $count"); + $indexes[] = sprintf('%s %d', $i_k, $count); $count = 1; $i_k = $k[$i]; } + $c_offset = $offsets[$k[$i]]; if (is_array($c_offset)) { $result .= pack('C', 2); - $result .= pack('N', $c_offset["stmoid"]); - $result .= pack('C', $c_offset["pos"]); + $result .= pack('N', $c_offset['stmoid']); + $result .= pack('C', $c_offset['pos']); } else { if ($c_offset === null) { $result .= pack('C', 0); - $result .= pack('N', $c_offset); - $result .= pack('C', 0); } else { $result .= pack('C', 1); - $result .= pack('N', $c_offset); - $result .= pack('C', 0); } + + $result .= pack('N', $c_offset); + $result .= pack('C', 0); } + $c_k = $k[$i]; } - array_push($indexes, "$i_k $count"); - $indexes = implode(" ", $indexes); + + $indexes[] = sprintf('%s %d', $i_k, $count); + $indexes = implode(' ', $indexes); // p_debug(show_bytes($result, 6)); return [ - "W" => [ 1, 4, 1 ], - "Index" => $indexes, - "stream" => $result + 'W' => [1, 4, 1], + 'Index' => $indexes, + 'stream' => $result, ]; } @@ -115,89 +121,92 @@ public static function build_xref_1_5($offsets) { * This function obtains the xref from the cross reference streams (7.5.8 Cross-Reference Streams) * which started in PDF 1.5. */ - public static function get_xref_1_5(&$_buffer, $xref_pos, $depth = null) { + public static function get_xref_1_5(&$_buffer, int $xref_pos, ?int $depth = null): false|array + { if ($depth !== null) { - if ($depth <= 0) + if ($depth <= 0) { return false; + } - $depth = $depth - 1; + --$depth; } - $xref_o = PDFUtilFnc::find_object_at_pos($_buffer, null, $xref_pos, []); - if ($xref_o === false) - return p_error("cross reference object not found when parsing xref at position $xref_pos", [false, false, false]); + $xref_o = self::find_object_at_pos($_buffer, null, $xref_pos, []); + if ($xref_o === false) { + throw new PDFException('cross reference object not found when parsing xref at position ' . $xref_pos, [false, false, false]); + } - if (!(isset($xref_o["Type"])) || ($xref_o["Type"]->val() !== "XRef")) - return p_error("invalid xref table", [false, false, false]); + if (! isset($xref_o['Type']) || $xref_o['Type']->val() !== 'XRef') { + throw new PDFException('invalid xref table', [false, false, false]); + } $stream = $xref_o->get_stream(false); - if ($stream === null) - return p_error("cross reference stream not found when parsing xref at position $xref_pos", [false, false, false]); + if ($stream === null) { + throw new PDFException('cross reference stream not found when parsing xref at position ' . $xref_pos, [false, false, false]); + } - $W = $xref_o["W"]->val(true); - if (count($W) !== 3) - return p_error("invalid cross reference object", [false, false, false]); + $W = $xref_o['W']->val(true); + if (count($W) !== 3) { + throw new PDFException('invalid cross reference object', [false, false, false]); + } - $W[0] = intval($W[0]); - $W[1] = intval($W[1]); - $W[2] = intval($W[2]); + $W[0] = (int) $W[0]; + $W[1] = (int) $W[1]; + $W[2] = (int) $W[2]; - $Size = $xref_o["Size"]->get_int(); - if ($Size === false) - return p_error("could not get the size of the xref table", [false, false, false]); + $Size = $xref_o['Size']->get_int(); + if ($Size === false) { + throw new PDFException('could not get the size of the xref table', [false, false, false]); + } - $Index = [ 0, $Size ]; - if (isset($xref_o["Index"])) - $Index = $xref_o["Index"]->val(true); + $Index = [0, $Size]; + if (isset($xref_o['Index'])) { + $Index = $xref_o['Index']->val(true); + } - if (count($Index) % 2 !== 0) - return p_error("invalid indexes of xref table", [false, false, false]); + if (count($Index) % 2 !== 0) { + throw new PDFException('invalid indexes of xref table', [false, false, false]); + } // Get the previous xref table, to build up on it - $trailer_obj = null; $xref_table = []; - if (($depth === null) || ($depth > 0)) { - // If still want to get more versions, let's check whether there is a previous xref table or not - - if (isset($xref_o["Prev"])) { - $Prev = $xref_o["Prev"]; - $Prev = $Prev->get_int(); - if ($Prev === false) - return p_error("invalid reference to a previous xref table", [false, false, false]); + // If still want to get more versions, let's check whether there is a previous xref table or not + if (($depth === null || $depth > 0) && isset($xref_o['Prev'])) { + $Prev = $xref_o['Prev']; + $Prev = $Prev->get_int(); + if ($Prev === false) { + throw new PDFException('invalid reference to a previous xref table', [false, false, false]); + } - // When dealing with 1.5 cross references, we do not allow to use other than cross references - [ $xref_table, $trailer_obj ] = PDFUtilFnc::get_xref_1_5($_buffer, $Prev, $depth); - // p_debug_var($xref_table); - } + // When dealing with 1.5 cross references, we do not allow to use other than cross references + [$xref_table] = self::get_xref_1_5($_buffer, $Prev, $depth); + // p_debug_var($xref_table); } // p_debug("xref table found at $xref_pos (oid: " . $xref_o->get_oid() . ")"); $stream_v = new StreamReader($stream); // Get the format function to un pack the values - $get_fmt_function = function($f) { - if ($f === false) + $get_fmt_function = static function ($f) { + if ($f === false) { return false; - - switch ($f) { - case 0: return function ($v) { return 0; }; - case 1: return function ($v) { return unpack('C', str_pad($v, 1, chr(0), STR_PAD_LEFT))[1]; }; - case 2: return function ($v) { return unpack('n', str_pad($v, 2, chr(0), STR_PAD_LEFT))[1]; }; - case 3: - case 4: return function ($v) { return unpack('N', str_pad($v, 4, chr(0), STR_PAD_LEFT))[1]; }; - case 5: - case 6: - case 7: - case 8: return function ($v) { return unpack('J', str_pad($v, 8, chr(0), STR_PAD_LEFT))[1]; }; } - return false; + + return match ($f) { + 0 => static fn ($v): int => 0, + 1 => static fn ($v) => unpack('C', str_pad($v, 1, chr(0), STR_PAD_LEFT))[1], + 2 => static fn ($v) => unpack('n', str_pad($v, 2, chr(0), STR_PAD_LEFT))[1], + 3, 4 => static fn ($v) => unpack('N', str_pad($v, 4, chr(0), STR_PAD_LEFT))[1], + 5, 6, 7, 8 => static fn ($v) => unpack('J', str_pad($v, 8, chr(0), STR_PAD_LEFT))[1], + default => false, + }; }; $fmt_function = [ $get_fmt_function($W[0]), $get_fmt_function($W[1]), - $get_fmt_function($W[2]) + $get_fmt_function($W[2]), ]; // p_debug("xref entries at $xref_pos for object " . $xref_o->get_oid()); @@ -209,13 +218,14 @@ public static function get_xref_1_5(&$_buffer, $xref_pos, $depth = null) { $object_i = $Index[$index_i++]; $object_count = $Index[$index_i++]; - while (($stream_v->currentchar() !== false) && ($object_count > 0)) { - $f1 = $W[0]!=0?($fmt_function[0]($stream_v->nextchars($W[0]))):1; + while ($stream_v->currentchar() !== false && $object_count > 0) { + $f1 = $W[0] != 0 ? $fmt_function[0]($stream_v->nextchars($W[0])) : 1; $f2 = $fmt_function[1]($stream_v->nextchars($W[1])); $f3 = $fmt_function[2]($stream_v->nextchars($W[2])); - if (($f1 === false) || ($f2 === false) || ($f3 === false)) - return p_error("invalid stream for xref table", [false, false, false]); + if ($f1 === false || $f2 === false || $f3 === false) { + throw new PDFException('invalid stream for xref table', [false, false, false]); + } switch ($f1) { case 0: @@ -225,20 +235,18 @@ public static function get_xref_1_5(&$_buffer, $xref_pos, $depth = null) { case 1: // Add object $xref_table[$object_i] = $f2; - /* - TODO: consider creating a generation table, but for the purpose of the xref there is no matter... if the document if well-formed. - */ - if ($f3 !== 0) - p_warning("Objects of non-zero generation are not fully checked... please double check your document and (if possible) please send examples via issues to https://github.com/dealfonso/sapp/issues/"); break; case 2: // Stream object // $f2 is the number of a stream object, $f3 is the index in that stream object - $xref_table[$object_i] = array("stmoid" => $f2, "pos" => $f3 ); + $xref_table[$object_i] = [ + 'stmoid' => $f2, + 'pos' => $f3, + ]; break; default: - p_error("do not know about entry of type $f1 in xref table"); + throw new PDFException(sprintf('do not know about entry of type %s in xref table', $f1)); } $object_i++; @@ -246,55 +254,58 @@ public static function get_xref_1_5(&$_buffer, $xref_pos, $depth = null) { } } - return [ $xref_table, $xref_o->get_value(), "1.5" ]; + return [$xref_table, $xref_o->get_value(), '1.5']; } - public static function get_xref_1_4(&$_buffer, $xref_pos, $depth = null) { + public static function get_xref_1_4(&$_buffer, string $xref_pos, $depth = null): false|array + { if ($depth !== null) { - if ($depth <= 0) + if ($depth <= 0) { return false; + } - $depth = $depth - 1; + --$depth; } - $trailer_pos = strpos($_buffer, "trailer", $xref_pos); - $min_pdf_version = "1.4"; + $trailer_pos = strpos((string) $_buffer, 'trailer', $xref_pos); + $min_pdf_version = '1.4'; // Get the xref content and make sure that the buffer passed contains the xref tag at the offset provided - $xref_substr = substr($_buffer, $xref_pos, $trailer_pos - $xref_pos); + $xref_substr = substr((string) $_buffer, $xref_pos, $trailer_pos - $xref_pos); $separator = "\r\n"; $xref_line = strtok($xref_substr, $separator); - if ($xref_line !== 'xref') - return p_error("xref tag not found at position $xref_pos", [false, false, false]); - + if ($xref_line !== 'xref') { + throw new PDFException('xref tag not found at position ' . $xref_pos, [false, false, false]); + } + // Now parse the lines and build the xref table $obj_id = false; $obj_count = 0; $xref_table = []; while (($xref_line = strtok($separator)) !== false) { - // The first type of entry contains the id of the next object and the amount of continuous objects defined - if (preg_match('/([0-9]+) ([0-9]+)$/', $xref_line, $matches) === 1) { + if (preg_match('/(\d+) (\d+)$/', $xref_line, $matches) === 1) { if ($obj_count > 0) { // If still expecting objects, we'll assume that the xref is malformed - return p_error("malformed xref at position $xref_pos", [false, false, false]); + throw new PDFException('malformed xref at position ' . $xref_pos, [false, false, false]); } - $obj_id = intval($matches[1]); - $obj_count = intval($matches[2]); + + $obj_id = (int) $matches[1]; + $obj_count = (int) $matches[2]; continue; } // The other type of entry contains the offset of the object, the generation and the command (which is "f" for "free" or "n" for "new") - if (preg_match('/^([0-9]+) ([0-9]+) (.)\s*/', $xref_line, $matches) === 1) { - + if (preg_match('/^(\d+) (\d+) (.)\s*/', $xref_line, $matches) === 1) { // If no object expected, we'll assume that the xref is malformed - if ($obj_count === 0) - return p_error("unexpected entry for xref: $xref_line", [false, false, false]); + if ($obj_count === 0) { + throw new PDFException('unexpected entry for xref: ' . $xref_line, [false, false, false]); + } - $obj_offset = intval($matches[1]); - $obj_generation = intval($matches[2]); + $obj_offset = (int) $matches[1]; + $obj_generation = (int) $matches[2]; $obj_operation = $matches[3]; if ($obj_offset !== 0) { @@ -320,155 +331,167 @@ public static function get_xref_1_4(&$_buffer, $xref_pos, $depth = null) { // in the actual offset. // TODO: consider creating a "generation table" $xref_table[$obj_id] = $obj_offset; - if ($obj_generation != 0) - p_warning("Objects of non-zero generation are not fully checked... please double check your document and (if possible) please send examples via issues to https://github.com/dealfonso/sapp/issues/"); + break; default: // If it is not one of the expected, let's skip the object - p_error("invalid entry for xref: $xref_line", [false, false, false]); + throw new PDFException('invalid entry for xref: ' . $xref_line, [false, false, false]); } } - $obj_count-= 1; + $obj_count--; $obj_id++; continue; } // If the entry is not recongised, show the error - p_error("invalid xref entry $xref_line"); - $xref_line = strtok($separator); + throw new PDFException('invalid xref entry ' . $xref_line); } // Get the trailer object - $trailer_obj = PDFUtilFnc::get_trailer($_buffer, $trailer_pos); + $trailer_obj = self::get_trailer($_buffer, $trailer_pos); // If there exists a previous xref (for incremental PDFs), get it and merge the objects that do not exist in the current xref table if (isset($trailer_obj['Prev'])) { - $xref_prev_pos = $trailer_obj['Prev']->val(); - if (!is_numeric($xref_prev_pos)) - return p_error("invalid trailer $trailer_obj", [false, false, false]); + if (! is_numeric($xref_prev_pos)) { + throw new PDFException('invalid trailer ' . $trailer_obj, [false, false, false]); + } - $xref_prev_pos = intval($xref_prev_pos); + $xref_prev_pos = (int) $xref_prev_pos; - [ $prev_table, $prev_trailer, $prev_min_pdf_version ] = PDFUtilFnc::get_xref_1_4($_buffer, $xref_prev_pos, $depth); + [$prev_table, $prev_trailer, $prev_min_pdf_version] = self::get_xref_1_4($_buffer, $xref_prev_pos, $depth); - if ($prev_min_pdf_version !== $min_pdf_version) - return p_error("mixed type of xref tables are not supported", [ false, false, false]); + if ($prev_min_pdf_version !== $min_pdf_version) { + throw new PDFException('mixed type of xref tables are not supported', [false, false, false]); + } if ($prev_table !== false) { - foreach ($prev_table as $obj_id => &$obj_offset) { // Not modifying the objects, but to make sure that it does not consume additional memory + foreach ($prev_table as $obj_id2 => $obj_offset) { // Not modifying the objects, but to make sure that it does not consume additional memory // If there not exists a new version, we'll acquire it - if (!isset($xref_table[$obj_id])) { - $xref_table[$obj_id] = $obj_offset; + if (! isset($xref_table[$obj_id2])) { + $xref_table[$obj_id2] = $obj_offset; } } } } - return [ $xref_table, $trailer_obj, $min_pdf_version ]; + return [$xref_table, $trailer_obj, $min_pdf_version]; } - public static function get_xref(&$_buffer, $xref_pos, $depth = null) { - + public static function get_xref(string &$_buffer, ?int $xref_pos, ?int $depth = null): array + { // Each xref is immediately followed by a trailer - $trailer_pos = strpos($_buffer, "trailer", $xref_pos); + $trailer_pos = strpos($_buffer, 'trailer', $xref_pos); if ($trailer_pos === false) { - [ $xref_table, $trailer_obj, $min_pdf_version ] = PDFUtilFnc::get_xref_1_5($_buffer, $xref_pos, $depth); - } else - [ $xref_table, $trailer_obj, $min_pdf_version ] = PDFUtilFnc::get_xref_1_4($_buffer, $xref_pos, $depth); - return [ $xref_table, $trailer_obj, $min_pdf_version ]; + [$xref_table, $trailer_obj, $min_pdf_version] = self::get_xref_1_5($_buffer, $xref_pos, $depth); + } else { + [$xref_table, $trailer_obj, $min_pdf_version] = self::get_xref_1_4($_buffer, $xref_pos, $depth); + } + + return [$xref_table, $trailer_obj, $min_pdf_version]; } - public static function acquire_structure(&$_buffer, $depth = null) { + public static function acquire_structure(string &$_buffer, ?int $depth = null): false|array + { // Get the first line and acquire the PDF version of the document $separator = "\r\n"; $pdf_version = strtok($_buffer, $separator); - if ($pdf_version === false) + if ($pdf_version === false) { return false; + } - if (preg_match('/^%PDF-[0-9]+\.[0-9]+$/', $pdf_version, $matches) !== 1) - return p_error("PDF version string not found"); + if (preg_match('/^%PDF-\d+\.\d+$/', $pdf_version, $matches) !== 1) { + throw new PDFException('PDF version string not found'); + } - if (preg_match_all('/startxref\s*([0-9]+)\s*%%EOF($|[\r\n])/ms', $_buffer, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE) === false) - return p_error("failed to get structure"); + if (preg_match_all('/startxref\s*([0-9]+)\s*%%EOF($|[\r\n])/ms', $_buffer, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE) === false) { + throw new PDFException('failed to get structure'); + } - $_versions=[]; + $_versions = []; /* print_r($matches); exit(); */ foreach ($matches as $match) { - array_push($_versions, $match[2][1] + strlen($match[2][0])); + $_versions[] = $match[2][1] + strlen($match[2][0]); } // Now get the trailing part and make sure that it has the proper form - $startxref_pos = strrpos($_buffer, "startxref"); - if ($startxref_pos === false) - return p_error("startxref not found"); + $startxref_pos = strrpos($_buffer, 'startxref'); + if ($startxref_pos === false) { + throw new PDFException('startxref not found'); + } - if (preg_match('/startxref\s*([0-9]+)\s*%%EOF\s*$/ms', $_buffer, $matches, 0, $startxref_pos) !== 1) - return p_error("startxref and %%EOF not found"); + if (preg_match('/startxref\s*([0-9]+)\s*%%EOF\s*$/ms', $_buffer, $matches, 0, $startxref_pos) !== 1) { + throw new PDFException('startxref and %%EOF not found'); + } - $xref_pos = intval($matches[1]); + $xref_pos = (int) $matches[1]; if ($xref_pos === 0) { // This is a dummy xref position from linearized documents return [ - "trailer" => false, - "version" => substr($pdf_version, 1), - "xref" => [], - "xrefposition" => 0, - "xrefversion" => substr($pdf_version, 1), - "revisions" => $_versions - ]; + 'trailer' => false, + 'version' => substr($pdf_version, 1), + 'xref' => [], + 'xrefposition' => 0, + 'xrefversion' => substr($pdf_version, 1), + 'revisions' => $_versions, + ]; } - [ $xref_table, $trailer_object, $min_pdf_version ] = PDFUtilFnc::get_xref($_buffer, $xref_pos, $depth); + [$xref_table, $trailer_object, $min_pdf_version] = self::get_xref($_buffer, $xref_pos, $depth); // We are providing a lot of information to be able to inspect the problems of a PDF file if ($xref_table === false) { // TODO: Maybe we could include a "recovery" method for this: if xref is not at pos $xref_pos, we could search for xref by hand - return p_error("could not find the xref table"); + throw new PDFException('could not find the xref table'); } - if ($trailer_object === false) - return p_error("could not find the trailer object"); + if ($trailer_object === false) { + throw new PDFException('could not find the trailer object'); + } return [ - "trailer" => $trailer_object, - "version" => substr($pdf_version, 1), - "xref" => $xref_table, - "xrefposition" => $xref_pos, - "xrefversion" => $min_pdf_version, - "revisions" => $_versions + 'trailer' => $trailer_object, + 'version' => substr($pdf_version, 1), + 'xref' => $xref_table, + 'xrefposition' => $xref_pos, + 'xrefversion' => $min_pdf_version, + 'revisions' => $_versions, ]; } /** * Function that finds a the object at the specific position in the buffer + * * @param buffer the buffer from which to read the document * @param oid the target object id to read (if null, will return the first object, if found) * @param offset the offset at which the object is expected to be * @param xref_table the xref table, to be able to find indirect objects + * * @return obj the PDFObject obtained from the file or false if could not be found */ - public static function find_object_at_pos(&$_buffer, $oid, $object_offset, $xref_table) { + public static function find_object_at_pos(&$_buffer, ?int $oid, int $object_offset, $xref_table): false|PDFObject + { + $object = self::object_from_string($_buffer, $oid, $object_offset, $offset_end); - $object = PDFUtilFnc::object_from_string($_buffer, $oid, $object_offset, $offset_end); - - if ($object === false) return false; + if ($object === false) { + return false; + } $_stream_pending = false; // The distinction is required, because we need to get the proper start for the stream, and if using CRLF instead of LF - // - according to https://www.adobe.com/content/dam/acom/en/devnet/pdf/PDF32000_2008.pdf, stream is followed by CRLF + // - according to https://www.adobe.com/content/dam/acom/en/devnet/pdf/PDF32000_2008.pdf, stream is followed by CRLF // or LF, but not single CR. - if (substr($_buffer, $offset_end - 7, 7) === "stream\n") { + if (substr((string) $_buffer, $offset_end - 7, 7) === "stream\n") { $_stream_pending = $offset_end; } - if (substr($_buffer, $offset_end - 7, 8) === "stream\r\n") { + if (substr((string) $_buffer, $offset_end - 7, 8) === "stream\r\n") { $_stream_pending = $offset_end + 1; } @@ -478,20 +501,22 @@ public static function find_object_at_pos(&$_buffer, $oid, $object_offset, $xref if ($length === false) { $length_object_id = $object['Length']->get_object_referenced(); if ($length_object_id === false) { - return p_error("could not get stream for object $obj_id"); + throw new PDFException('could not get stream for object ' . $oid); + } + + $length_object = self::find_object($_buffer, $xref_table, $length_object_id); + if ($length_object === false) { + throw new PDFException('could not get object ' . $oid); } - $length_object = PDFUtilFnc::find_object($_buffer, $xref_table, $length_object_id); - if ($length_object === false) - return p_error("could not get object $oid"); - $length = $length_object->get_value()->get_int(); + $length = $length_object->get_value()?->get_int(); } if ($length === false) { - return p_error("could not get stream length for object $obj_id"); + throw new PDFException('could not get stream length for object ' . $oid); } - $object->set_stream(substr($_buffer, $_stream_pending, $length), true); + $object->set_stream(substr((string) $_buffer, $_stream_pending, $length), true); } return $object; @@ -499,103 +524,120 @@ public static function find_object_at_pos(&$_buffer, $oid, $object_offset, $xref /** * Function that finds a specific object in the document, using the xref table as a base + * * @param buffer the buffer from which to read the document * @param xref_table the xref table - * @param oid the target object id to read + * @param oid the target object id to read + * * @return obj the PDFObject obtained from the file or false if could not be found */ - public static function find_object(&$_buffer, $xref_table, $oid) { - - if ($oid === 0) return false; - if (!isset($xref_table[$oid])) return false; + public static function find_object(&$_buffer, $xref_table, int $oid): false|PDFObject + { + if ($oid === 0) { + return false; + } + + if (! isset($xref_table[$oid])) { + return false; + } // Find the object and get where it ends $object_offset = $xref_table[$oid]; - if (!is_array($object_offset)) - return PDFUtilFnc::find_object_at_pos($_buffer, $oid, $object_offset, $xref_table); - else { - $object = PDFUtilFnc::find_object_in_objstm($_buffer, $xref_table, $object_offset["stmoid"], $object_offset["pos"], $oid); - return $object; - } + if (! is_array($object_offset)) { + return self::find_object_at_pos($_buffer, $oid, $object_offset, $xref_table); + } + + return self::find_object_in_objstm($_buffer, $xref_table, $object_offset['stmoid'], $object_offset['pos'], $oid); } /** * Function that searches for an object in an object stream */ - public static function find_object_in_objstm(&$_buffer, $xref_table, $objstm_oid, $objpos, $oid) { - $objstm = PDFUtilFnc::find_object($_buffer, $xref_table, $objstm_oid); - if ($objstm === false) - return p_error("could not get object stream $objstm_oid"); + public static function find_object_in_objstm(&$_buffer, $xref_table, int $objstm_oid, $objpos, int $oid): PDFObject + { + $objstm = self::find_object($_buffer, $xref_table, $objstm_oid); + if ($objstm === false) { + throw new PDFException('could not get object stream ' . $objstm_oid); + } - if (($objstm["Extends"]??false !== false)) - // TODO: support them - return p_error("not supporting extended object streams at this time"); + if (($objstm['Extends'] ?? false) !== false) { // TODO: support them + throw new PDFException('not supporting extended object streams at this time'); + } - $First = $objstm["First"]??false; - $N = $objstm["N"]??false; - $Type = $objstm["Type"]??false; - - if (($First === false) || ($N === false) || ($Type === false)) - return p_error("invalid object stream $objstm_oid"); + $First = $objstm['First'] ?? false; + $N = $objstm['N'] ?? false; + $Type = $objstm['Type'] ?? false; - if ($Type->val() !== "ObjStm") - return p_error("object $objstm_oid is not an object stream"); + if ($First === false || $N === false || $Type === false) { + throw new PDFException('invalid object stream ' . $objstm_oid); + } + + if ($Type->val() !== 'ObjStm') { + throw new PDFException(sprintf('object %d is not an object stream', $objstm_oid)); + } $First = $First->get_int(); - $N = $N->get_int(); + assert($N instanceof PDFValue); $stream = $objstm->get_stream(false); - $index = substr($stream, 0, $First); - $index = explode(" ", trim($index)); - $stream = substr($stream, $First); + $index = substr((string) $stream, 0, $First); + $index = explode(' ', trim($index)); - if (count($index) % 2 !== 0) - return p_error("invalid index for object stream $objstm_oid"); + $stream = substr((string) $stream, $First); - $objpos = $objpos * 2; - if ($objpos > count($index)) - return p_error("object $oid not found in object stream $objstm_oid"); + if (count($index) % 2 !== 0) { + throw new PDFException('invalid index for object stream ' . $objstm_oid); + } + + $objpos *= 2; + if ($objpos > count($index)) { + throw new PDFException(sprintf('object %d not found in object stream %d', $oid, $objstm_oid)); + } - $offset = intval($index[$objpos + 1]); - $next = 0; + $offset = (int) $index[$objpos + 1]; $offsets = []; - for ($i = 1; ($i < count($index)); $i = $i + 2) - array_push($offsets, intval($index[$i])); + $counter = count($index); + for ($i = 1; $i < $counter; $i += 2) { + $offsets[] = (int) $index[$i]; + } - array_push($offsets, strlen($stream)); + $offsets[] = strlen($stream); sort($offsets); - for ($i = 0; ($i < count($offsets)) && ($offset >= $offsets[$i]); $i++); + for ($i = 0; $i < count($offsets) && $offset >= $offsets[$i]; $i++) { + } $next = $offsets[$i]; - $object_def_str = "$oid 0 obj " . substr($stream, $offset, $next - $offset) . " endobj"; - $object_def = PDFUtilFnc::object_from_string($object_def_str, $oid); - return $object_def; + $object_def_str = $oid . ' 0 obj ' . substr($stream, $offset, $next - $offset) . ' endobj'; + + return self::object_from_string($object_def_str, $oid); } /** - * Function that parses an object + * Function that parses an object */ - public static function object_from_string(&$buffer, $expected_obj_id, $offset = 0, &$offset_end = 0) { + public static function object_from_string(string $buffer, ?int $expected_obj_id, int $offset = 0, ?int &$offset_end = 0): PDFObject + { if (preg_match('/([0-9]+)\s+([0-9+])\s+obj(\s+)/ms', $buffer, $matches, 0, $offset) !== 1) { // p_debug_var(substr($buffer)) - return p_error("object is not valid: $expected_obj_id"); + throw new PDFException('object is not valid: ' . $expected_obj_id); } $found_obj_header = $matches[0]; - $found_obj_id = intval($matches[1]); - $found_obj_generation = intval($matches[2]); + $found_obj_id = (int) $matches[1]; + $found_obj_generation = (int) $matches[2]; - if ($expected_obj_id === null) + if ($expected_obj_id === null) { $expected_obj_id = $found_obj_id; + } if ($found_obj_id !== $expected_obj_id) { - return p_error("pdf structure is corrupt: found obj $found_obj_id while searching for obj $expected_obj_id (at $offset)"); + throw new PDFException(sprintf('pdf structure is corrupt: found obj %d while searching for obj %d (at %d)', $found_obj_id, $expected_obj_id, $offset)); } // The object starts after the header - $offset = $offset + strlen($found_obj_header); + $offset += strlen($found_obj_header); // Parse the object $parser = new PDFObjectParser(); @@ -603,8 +645,9 @@ public static function object_from_string(&$buffer, $expected_obj_id, $offset = $stream = new StreamReader($buffer, $offset); $obj_parsed = $parser->parse($stream); - if ($obj_parsed === false) - return p_error("object $expected_obj_id could not be parsed"); + if ($obj_parsed === false) { + throw new PDFException(sprintf('object %d could not be parsed', $expected_obj_id)); + } switch ($parser->current_token()) { case PDFObjectParser::T_OBJECT_END: @@ -614,43 +657,53 @@ public static function object_from_string(&$buffer, $expected_obj_id, $offset = // There is an stream break; default: - return p_error("malformed object"); + throw new PDFException('malformed object'); } $offset_end = $stream->getpos(); + return new PDFObject($found_obj_id, $obj_parsed, $found_obj_generation); } /** * Builds the xref for the document, using the list of objects + * * @param offsets an array indexed by the oid of the objects, with the offset of each * object in the document. + * * @return xref_string a string that contains the xref table, ready to be inserted in the document */ - public static function build_xref($offsets) { + public static function build_xref(array $offsets): string + { $k = array_keys($offsets); sort($k); $i_k = 0; $c_k = 0; $count = 1; - $result = ""; + $result = ''; $references = "0000000000 65535 f \n"; - for ($i = 0; $i < count($k); $i++) { - if ($k[$i] === 0) continue; + for ($i = 0, $iMax = count($k); $i < $iMax; $i++) { + if ($k[$i] === 0) { + continue; + } + if ($k[$i] === $c_k + 1) { $count++; } else { - $result = $result . "$i_k {$count}\n$references"; + $result .= sprintf('%s %d%s%s', $i_k, $count, PHP_EOL, $references); $count = 1; $i_k = $k[$i]; - $references = ""; + $references = ''; } + $references .= sprintf("%010d 00000 n \n", $offsets[$k[$i]]); $c_k = $k[$i]; } - $result = $result . "$i_k {$count}\n$references"; - return "xref\n$result"; - } + $result .= sprintf('%s %d%s%s', $i_k, $count, PHP_EOL, $references); + + return 'xref +' . $result; + } } diff --git a/src/helpers/Buffer.php b/src/helpers/Buffer.php index 88b8f9d..5ac55da 100644 --- a/src/helpers/Buffer.php +++ b/src/helpers/Buffer.php @@ -21,120 +21,156 @@ namespace ddn\sapp\helpers; -if (!defined('__CONVENIENT_MAX_BUFFER_DUMP')) - define('__CONVENIENT_MAX_BUFFER_DUMP', 80); +use Exception; +use Stringable; -use function ddn\sapp\helpers\debug_var; +if (! defined('__CONVENIENT_MAX_BUFFER_DUMP')) { + define('__CONVENIENT_MAX_BUFFER_DUMP', 80); +} /** * This class is used to manage a buffer of characters. The main features are that * it is possible to add data (by usign *data* function), and getting the current * size. Then it is possible to get the whole buffer using function *get_raw* */ -class Buffer { - protected $_buffer = ""; - protected $_bufferlen = 0; +class Buffer implements Stringable +{ + protected ?string $_buffer; + + protected int $_bufferlen; - public function __construct($string = null) { - if ($string === null) - $string = ""; + public function __construct(?string $string = null) + { + if ($string === null) { + $string = ''; + } $this->_buffer = $string; $this->_bufferlen = strlen($string); } + + /** + * Provides a easy to read string representation of the buffer, using the "var_dump" output + * of the variable, but providing a reduced otput of the buffer + * + * @return str a string with the representation of the buffer + */ + public function __toString(): string + { + if (strlen((string) $this->_buffer) < __CONVENIENT_MAX_BUFFER_DUMP * 2) { + return (string) debug_var($this); + } + + $buffer = $this->_buffer; + $this->_buffer = substr((string) $buffer, 0, __CONVENIENT_MAX_BUFFER_DUMP); + $this->_buffer .= "\n...\n" . substr((string) $buffer, -__CONVENIENT_MAX_BUFFER_DUMP); + $result = debug_var($this); + $this->_buffer = $buffer; + + return (string) $result; + } + /** * Adds raw data to the buffer + * * @param data the data to add */ - public function data(...$datas) { + public function data(...$datas): void + { foreach ($datas as $data) { - $this->_bufferlen += strlen($data); + $this->_bufferlen += strlen((string) $data); $this->_buffer .= $data; } - } + } + /** * Obtains the size of the buffer + * * @return size the size of the buffer */ - public function size() { + public function size(): int + { return $this->_bufferlen; } + /** * Gets the raw data from the buffer + * * @return buffer the raw data */ - public function get_raw() { + public function get_raw(): ?string + { return $this->_buffer; } + /** * Appends buffer $b to this buffer + * * @param b the buffer to be added to this one + * * @return buffer this object */ - public function append($b) { - if (get_class($b) !== get_class($this)) + public function append($b): static + { + if ($b::class !== static::class) { throw new Exception('invalid buffer to add to this one'); - + } + $this->_buffer .= $b->get_raw(); $this->_bufferlen = strlen($this->_buffer); + return $this; } + /** * Obtains a new buffer that is the result from the concatenation of this buffer and the parameter + * * @param b the buffer to be added to this one + * * @return buffer the resulting buffer (different from this one) */ - public function add(...$bs) { + public function add(...$bs): self + { foreach ($bs as $b) { - if (get_class($b) !== get_class($this)) + if ($b::class !== static::class) { throw new Exception('invalid buffer to add to this one'); + } } - $r = new Buffer($this->_buffer); - foreach ($bs as $b) + $r = new self($this->_buffer); + foreach ($bs as $b) { $r->append($b); + } return $r; } + /** * Returns a new buffer that contains the same data than this one + * * @return buffer the cloned buffer */ - public function clone() { - $buffer = new Buffer($this->_buffer); - return $buffer; + public function clone(): self + { + return new self($this->_buffer); } - /** - * Provides a easy to read string representation of the buffer, using the "var_dump" output - * of the variable, but providing a reduced otput of the buffer - * @return str a string with the representation of the buffer - */ - public function __toString() { - if (strlen($this->_buffer) < (__CONVENIENT_MAX_BUFFER_DUMP * 2)) - return debug_var($this); - - $buffer = $this->_buffer; - $this->_buffer = substr($buffer, 0, __CONVENIENT_MAX_BUFFER_DUMP); - $this->_buffer .= "\n...\n" . substr($buffer, -__CONVENIENT_MAX_BUFFER_DUMP); - $result = debug_var($this); - $this->_buffer = $buffer; - return $result; - } - - public function show_bytes($columns, $offset = 0, $length = null) { - if ($length === null) + public function show_bytes($columns, $offset = 0, $length = null): string + { + if ($length === null) { $length = $this->_bufferlen; + } - $result = ""; + $result = ''; $length = min($length, $this->_bufferlen); for ($i = $offset; $i < $length;) { - for ($j = 0; ($j < $columns) && ($i < $length); $i++, $j++) { - $result .= sprintf("%02x ", ord($this->_buffer[$i])); + for ($j = 0; $j < $columns && $i < $length; $i++, $j++) { + $result .= sprintf('%02x ', ord($this->_buffer[$i])); } + $result .= "\n"; } return $result; } -} \ No newline at end of file +} diff --git a/src/helpers/CMS.php b/src/helpers/CMS.php index 799c107..1cda93b 100644 --- a/src/helpers/CMS.php +++ b/src/helpers/CMS.php @@ -1,5 +1,7 @@ $r) { - if (stripos($r, 'HTTP/') === 0) { - list(,$code, $status) = explode(' ', $r, 3); - break; + + /** + * send tsa/ocsp query with curl + * + * @return string response body + * @public + */ + public function sendReq(array $reqData): string + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $reqData['uri']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: ' . $reqData['req_contentType'], 'User-Agent: SAPP PDF']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $reqData['data']); + if (($reqData['user'] ?? null) && ($reqData['password'] ?? null)) { + curl_setopt($ch, CURLOPT_USERPWD, $reqData['user'] . ':' . $reqData['password']); } - } - if($code != '200') { - p_error(" response error! Code=\"$code\", Status=\"".trim($status??"")."\""); - return false; - } - $contentTypeHeader = ''; - $headers = explode("\n", $header); - foreach ($headers as $key => $r) { - // Match the header name up to ':', compare lower case - if (stripos($r, "Content-Type".':') === 0) { - list($headername, $headervalue) = explode(":", $r, 2); - $contentTypeHeader = trim($headervalue); + + $tsResponse = curl_exec($ch); + + if (! $tsResponse) { + throw new PDFException('empty curl response'); } - } - if($contentTypeHeader != $reqData['resp_contentType']) { - p_error(" response content type not {$reqData['resp_contentType']}, but: \"$contentTypeHeader\""); - return false; - } - if(empty($body)) { - p_error(' error empty response!'); - } - return $body; // binary response - } - } - - /** - * parse tsa response to array - * @param string $binaryTsaRespData binary tsa response to parse - * @return array asn.1 hex structure of tsa response - */ - private function tsa_parseResp($binaryTsaRespData) { - if(!@$ar = asn1::parse(bin2hex($binaryTsaRespData), 3)) { - p_error(" can't parse invalid tsa Response."); - return false; - } - $curr = $ar; - foreach($curr as $key=>$value) { - if($value['type'] == '30') { - $curr['TimeStampResp']=$curr[$key]; - unset($curr[$key]); - } - } - $ar=$curr; - $curr = $ar['TimeStampResp']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30' && !array_key_exists('status', $curr)) { - $curr['status']=$curr[$key]; - unset($curr[$key]); - } else if($value['type'] == '30') { - $curr['timeStampToken']=$curr[$key]; - unset($curr[$key]); + + $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + curl_close($ch); + $header = substr($tsResponse, 0, $header_size); + $body = substr($tsResponse, $header_size); + // Get the HTTP response code + $headers = explode("\n", $header); + foreach ($headers as $r) { + if (stripos($r, 'HTTP/') === 0) { + [, $code, $status] = explode(' ', $r, 3); + break; + } } - } - } - $ar['TimeStampResp']=$curr; - $curr = $ar['TimeStampResp']['timeStampToken']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '06') { - $curr['contentType']=$curr[$key]; - unset($curr[$key]); + + if ($code !== '200') { + throw new PDFException(sprintf('response error! Code="{$code}", Status="%s"', trim($status ?? ''))); } - if($value['type'] == 'a0') { - $curr['content']=$curr[$key]; - unset($curr[$key]); + + $contentTypeHeader = ''; + $headers = explode("\n", $header); + foreach ($headers as $r) { + // Match the header name up to ':', compare lower case + if (stripos($r, 'Content-Type:') === 0) { + [, $headervalue] = explode(':', $r, 2); + $contentTypeHeader = trim($headervalue); + } } - } - } - $ar['TimeStampResp']['timeStampToken'] = $curr; - $curr = $ar['TimeStampResp']['timeStampToken']['content']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30') { - $curr['TSTInfo']=$curr[$key]; - unset($curr[$key]); + + if ($contentTypeHeader != $reqData['resp_contentType']) { + throw new PDFException(sprintf('response content type not %s, but: "%s"', $reqData['resp_contentType'], $contentTypeHeader)); } - } - } - $ar['TimeStampResp']['timeStampToken']['content'] = $curr; - if(@$ar['TimeStampResp']['timeStampToken']['content']['hexdump'] != '') { - return $ar; - } else { - return false; - } - } - - /** - * Create timestamp query - * @param string $data binary data to hashed/digested - * @param string $hashAlg hash algorithm - * @return string hex TSTinfo. - */ - protected function createTimestamp($data, $hashAlg='sha1') { - $TSTInfo=false; - $tsaQuery = x509::tsa_query($data, $hashAlg); - $tsaData = $this->signature_data['tsa']; - $reqData = array( - 'data'=>$tsaQuery, - 'uri'=>$tsaData['host'], - 'req_contentType'=>'application/timestamp-query', - 'resp_contentType'=>'application/timestamp-reply' - ) + $tsaData; - - p_debug(" sending TSA query to \"".$tsaData['host']."\"..."); - if(!$binaryTsaResp = self::sendReq($reqData)) { - p_error(" TSA query send FAILED!"); - } else { - p_debug(" TSA query send OK"); - p_debug(" Parsing Timestamp response..."); - if(!$tsaResp = $this->tsa_parseResp($binaryTsaResp)) { - p_error(" parsing FAILED!"); - } - p_debug(" parsing OK"); - $TSTInfo = $tsaResp['TimeStampResp']['timeStampToken']['hexdump']; - } - return $TSTInfo; - } - - /** - * Perform OCSP/CRL Validation - * @param array $parsedCert parsed certificate - * @param string $ocspURI - * @param string $crlURIorFILE - * @param string $issuerURIorFILE - * @return array - */ - protected function LTVvalidation($parsedCert, $ocspURI=null, $crlURIorFILE=null, $issuerURIorFILE=false) { - $ltvResult['issuer']=false; - $ltvResult['ocsp']=false; - $ltvResult['crl']=false; - $certSigner_parse = $parsedCert; - p_debug(" getting OCSP & CRL address..."); - p_debug(" reading AIA OCSP attribute..."); - $ocspURI = @$certSigner_parse['tbsCertificate']['attributes']['1.3.6.1.5.5.7.1.1']['value']['1.3.6.1.5.5.7.48.1'][0]; - if(empty(trim($ocspURI))) { - p_warning(" FAILED!"); - } else { - p_debug(" OK got address:\"$ocspURI\""); + + if ($body === '' || $body === '0') { + throw new PDFException('error empty response!'); + } + + return $body; // binary response } - $ocspURI = trim($ocspURI); - p_debug(" reading CRL CDP attribute..."); - $crlURIorFILE = @$certSigner_parse['tbsCertificate']['attributes']['2.5.29.31']['value'][0]; - if(empty(trim($crlURIorFILE??""))) { - p_warning(" FAILED!"); - } else { - p_debug(" OK got address:\"$crlURIorFILE\""); - } - if(empty($ocspURI) && empty($crlURIorFILE)) { - p_error(" can't get OCSP/CRL address! Process terminated."); - } else { // Perform if either ocspURI/crlURIorFILE exists - p_debug(" getting Issuer..."); - p_debug(" looking for issuer address from AIA attribute..."); - $issuerURIorFILE = @$certSigner_parse['tbsCertificate']['attributes']['1.3.6.1.5.5.7.1.1']['value']['1.3.6.1.5.5.7.48.2'][0]; - $issuerURIorFILE = trim($issuerURIorFILE??""); - if(empty($issuerURIorFILE)) { - p_debug(" Failed!"); - } else { - p_debug(" OK got address \"$issuerURIorFILE\"..."); - p_debug(" load issuer from \"$issuerURIorFILE\"..."); - if($issuerCert = @file_get_contents($issuerURIorFILE)) { - p_debug(" OK. size ".round(strlen($issuerCert)/1024,2)."Kb"); - p_debug(" reading issuer certificate..."); - if($issuer_certDER = x509::get_cert($issuerCert)) { - p_debug(" OK"); - p_debug(" check if issuer is cert issuer..."); - $certIssuer_parse = x509::readcert($issuer_certDER, 'oid'); // Parsing Issuer cert - $certSigner_signatureField = $certSigner_parse['signatureValue']; - if(openssl_public_decrypt(hex2bin($certSigner_signatureField), $decrypted, x509::x509_der2pem($issuer_certDER), OPENSSL_PKCS1_PADDING)) { - p_debug(" OK issuer is cert issuer."); - $ltvResult['issuer'] = $issuer_certDER; + + /** + * Perform PKCS7 Signing + * + * @return string hex + padding 0 + * @public + */ + public function pkcs7_sign(string $binaryData): string + { + $hexOidHashAlgos = [ + 'md2' => '06082A864886F70D0202', + 'md4' => '06082A864886F70D0204', + 'md5' => '06082A864886F70D0205', + 'sha1' => '06052B0E03021A', + 'sha224' => '0609608648016503040204', + 'sha256' => '0609608648016503040201', + 'sha384' => '0609608648016503040202', + 'sha512' => '0609608648016503040203', + ]; + $hashAlgorithm = $this->signature_data['hashAlgorithm']; + if (! array_key_exists($hashAlgorithm, $hexOidHashAlgos)) { + throw new PDFException('not support hash algorithm!'); + } + + $this->logger?->debug(sprintf('hash algorithm is "%s"', $hashAlgorithm)); + $x509 = new x509(); + if (! $certParse = $x509::readcert($this->signature_data['signcert'])) { + throw new PDFException('certificate error! check certificate'); + } + + $hexEmbedCerts[] = bin2hex($x509::get_cert($this->signature_data['signcert'])); + $appendLTV = ''; + $ltvData = $this->signature_data['ltv']; + if (! empty($ltvData)) { + $this->logger?->debug(' LTV Validation start...'); + $LTVvalidation_ocsp = ''; + $LTVvalidation_crl = ''; + $LTVvalidationEnd = false; + + $isRootCA = false; + // check whether root ca + if ($certParse['tbsCertificate']['issuer']['hexdump'] == $certParse['tbsCertificate']['subject']['hexdump'] && openssl_public_decrypt(hex2bin((string) $certParse['signatureValue']), $decrypted, x509::x509_der2pem($x509::get_cert($this->signature_data['signcert'])), OPENSSL_PKCS1_PADDING)) { + $this->logger?->debug(sprintf('***** "%s" is a ROOT CA. No validation performed ***', $certParse['tbsCertificate']['subject']['2.5.4.3'][0])); + $isRootCA = true; + } + + if ($isRootCA == false) { + $i = 0; + $LTVvalidation = true; + $certtoCheck = $certParse; + while ($LTVvalidation !== false) { + $this->logger?->debug(sprintf('========= %d checking "%s"===============', $i, $certtoCheck['tbsCertificate']['subject']['2.5.4.3'][0])); + $LTVvalidation = $this->LTVvalidation($certtoCheck); + $i++; + if ($LTVvalidation) { + $curr_issuer = $LTVvalidation['issuer']; + $certtoCheck = $x509::readcert($curr_issuer, 'oid'); + if (@$LTVvalidation['ocsp'] || @$LTVvalidation['crl']) { + $LTVvalidation_ocsp .= $LTVvalidation['ocsp']; + $LTVvalidation_crl .= $LTVvalidation['crl']; + $hexEmbedCerts[] = bin2hex((string) $LTVvalidation['issuer']); + } + + // check whether root ca + if ($certtoCheck['tbsCertificate']['issuer']['hexdump'] == $certtoCheck['tbsCertificate']['subject']['hexdump'] && openssl_public_decrypt(hex2bin((string) $certtoCheck['signatureValue']), $decrypted, $x509::x509_der2pem($curr_issuer), OPENSSL_PKCS1_PADDING)) { + $this->logger?->debug(sprintf('========= FINISH Reached ROOT CA "%s"===============', $certtoCheck['tbsCertificate']['subject']['2.5.4.3'][0])); + $LTVvalidationEnd = true; + break; + } + } + } + + if ($LTVvalidationEnd) { + $this->logger?->debug(" LTV Validation SUCCESS\n"); + $ocsp = ''; + if ($LTVvalidation_ocsp !== '' && $LTVvalidation_ocsp !== '0') { + $ocsp = asn1::expl( + 1, + asn1::seq( + $LTVvalidation_ocsp + ) + ); + } + + $crl = ''; + if ($LTVvalidation_crl !== '' && $LTVvalidation_crl !== '0') { + $crl = asn1::expl( + 0, + asn1::seq( + $LTVvalidation_crl + ) + ); + } + + $appendLTV = asn1::seq( + '06092A864886F72F010108' . // adbe-revocationInfoArchival (1.2.840.113583.1.1.8) + asn1::set( + asn1::seq( + $ocsp . + $crl + ) + ) + ); + } else { + $this->logger?->warning("LTV Validation FAILED!\n"); + } + } + + foreach ($this->signature_data['extracerts'] ?? [] as $extracert) { + $hex_extracert = bin2hex($x509::x509_pem2der($extracert)); + if (! in_array($hex_extracert, $hexEmbedCerts, true)) { + $hexEmbedCerts[] = $hex_extracert; + } + } + } + + $messageDigest = hash($hashAlgorithm, $binaryData); + $authenticatedAttributes = asn1::seq( + '06092A864886F70D010903' . //OBJ_pkcs9_contentType 1.2.840.113549.1.9.3 + asn1::set('06092A864886F70D010701') //OBJ_pkcs7_data 1.2.840.113549.1.7.1 + ) . + asn1::seq( // signing time + '06092A864886F70D010905' . //OBJ_pkcs9_signingTime 1.2.840.113549.1.9.5 + asn1::set( + asn1::utime(date('ymdHis')) //UTTC Time + ) + ) . + asn1::seq( // messageDigest + '06092A864886F70D010904' . //OBJ_pkcs9_messageDigest 1.2.840.113549.1.9.4 + asn1::set(asn1::oct($messageDigest)) + ) . + $appendLTV; + $tohash = asn1::set($authenticatedAttributes); + $hash = hash($hashAlgorithm, hex2bin($tohash)); + $toencrypt = asn1::seq( + asn1::seq($hexOidHashAlgos[$hashAlgorithm] . '0500') . // OBJ $messageDigest & OBJ_null + asn1::oct($hash) + ); + $pkey = $this->signature_data['privkey']; + if (! openssl_private_encrypt(hex2bin($toencrypt), $encryptedDigest, $pkey, OPENSSL_PKCS1_PADDING)) { + throw new PDFException("openssl_private_encrypt error! can't encrypt"); + } + + $hexencryptedDigest = bin2hex($encryptedDigest); + $timeStamp = ''; + if (! empty($this->signature_data['tsa'])) { + $this->logger?->debug(' Timestamping process start...'); + if ($TSTInfo = $this->createTimestamp($encryptedDigest, $hashAlgorithm)) { + $this->logger?->debug(' Timestamping SUCCESS.'); + $TimeStampToken = asn1::seq( + '060B2A864886F70D010910020E' . // OBJ_id_smime_aa_timeStampToken 1.2.840.113549.1.9.16.2.14 + asn1::set($TSTInfo) + ); + $timeStamp = asn1::expl(1, $TimeStampToken); } else { - p_warning(" FAILED! issuer is not cert issuer."); + $this->logger?->warning('Timestamping FAILED!'); } - } else { - p_warning(" FAILED!"); - } + } + + $issuerName = $certParse['tbsCertificate']['issuer']['hexdump']; + $serialNumber = $certParse['tbsCertificate']['serialNumber']; + $signerinfos = asn1::seq( + asn1::int('1') . + asn1::seq($issuerName . asn1::int($serialNumber)) . + asn1::seq($hexOidHashAlgos[$hashAlgorithm] . '0500') . + asn1::expl(0, $authenticatedAttributes) . + asn1::seq( + '06092A864886F70D0101010500' + ) . + asn1::oct($hexencryptedDigest) . + $timeStamp + ); + $certs = asn1::expl(0, implode('', $hexEmbedCerts)); + $pkcs7contentSignedData = asn1::seq( + asn1::int('1') . + asn1::set(asn1::seq($hexOidHashAlgos[$hashAlgorithm] . '0500')) . + asn1::seq('06092A864886F70D010701') . //OBJ_pkcs7_data + $certs . + asn1::set($signerinfos) + ); + + return asn1::seq( + '06092A864886F70D010702' . // Hexadecimal form of pkcs7-signedData + asn1::expl(0, $pkcs7contentSignedData) + ); + } + + /** + * Create timestamp query + * + * @param string $data binary data to hashed/digested + * @param string $hashAlg hash algorithm + * + * @return string hex TSTinfo. + */ + protected function createTimestamp(string $data, string $hashAlg = 'sha1') + { + $tsaQuery = x509::tsa_query($data, $hashAlg); + $tsaData = $this->signature_data['tsa']; + $reqData = [ + 'data' => $tsaQuery, + 'uri' => $tsaData['host'], + 'req_contentType' => 'application/timestamp-query', + 'resp_contentType' => 'application/timestamp-reply', + ] + $tsaData; + + $this->logger?->debug(' sending TSA query to "' . $tsaData['host'] . '"...'); + if (($binaryTsaResp = $this->sendReq($reqData)) === '' || ($binaryTsaResp = $this->sendReq($reqData)) === '0') { + throw new PDFException('TSA query send FAILED!'); + } + + $this->logger?->debug(' TSA query send OK'); + $this->logger?->debug(' Parsing Timestamp response...'); + if (! $tsaResp = $this->tsa_parseResp($binaryTsaResp)) { + throw new PDFException('parsing FAILED!'); + } + + $this->logger?->debug(' parsing OK'); + + return $tsaResp['TimeStampResp']['timeStampToken']['hexdump']; + } + + /** + * Perform OCSP/CRL Validation + * + * @param array $parsedCert parsed certificate + * + * @return array + */ + protected function LTVvalidation(array $parsedCert): false|array + { + $ltvResult['issuer'] = false; + $ltvResult['ocsp'] = false; + $ltvResult['crl'] = false; + $certSigner_parse = $parsedCert; + $this->logger?->debug(' getting OCSP & CRL address...'); + $this->logger?->debug(' reading AIA OCSP attribute...'); + $ocspURI = @$certSigner_parse['tbsCertificate']['attributes']['1.3.6.1.5.5.7.1.1']['value']['1.3.6.1.5.5.7.48.1'][0]; + if (trim((string) $ocspURI) === '' || trim((string) $ocspURI) === '0') { + $this->logger?->warning('FAILED!'); } else { - p_warning(" FAILED!."); + $this->logger?->debug(sprintf('OK got address:"%s"', $ocspURI)); } - } - - if(!$ltvResult['issuer']) { - p_debug(" search for issuer in extracerts....."); - if(array_key_exists('extracerts', $this->signature_data) && ($this->signature_data['extracerts'] !== null) && (count($this->signature_data['extracerts']) > 0)) { - $i=0; - foreach($this->signature_data['extracerts'] as $extracert) { - p_debug(" extracerts[$i] ..."); - $certSigner_signatureField = $certSigner_parse['signatureValue']; - if(openssl_public_decrypt(hex2bin($certSigner_signatureField), $decrypted, $extracert, OPENSSL_PKCS1_PADDING)) { - p_debug(" OK got issuer."); - $certIssuer_parse = x509::readcert($extracert, 'oid'); // Parsing Issuer cert - $ltvResult['issuer'] = x509::get_cert($extracert); + + $ocspURI = trim((string) $ocspURI); + $this->logger?->debug(' reading CRL CDP attribute...'); + $crlURIorFILE = @$certSigner_parse['tbsCertificate']['attributes']['2.5.29.31']['value'][0]; + if (trim($crlURIorFILE ?? '') === '' || trim($crlURIorFILE ?? '') === '0') { + $this->logger?->warning('FAILED!'); + } else { + $this->logger?->debug(sprintf('OK got address:"%s"', $crlURIorFILE)); + } + + if (($ocspURI === '' || $ocspURI === '0') && empty($crlURIorFILE)) { + throw new PDFException("can't get OCSP/CRL address! Process terminated."); + } + + // Perform if either ocspURI/crlURIorFILE exists + $this->logger?->debug(' getting Issuer...'); + $this->logger?->debug(' looking for issuer address from AIA attribute...'); + $issuerURIorFILE = @$certSigner_parse['tbsCertificate']['attributes']['1.3.6.1.5.5.7.1.1']['value']['1.3.6.1.5.5.7.48.2'][0]; + $issuerURIorFILE = trim($issuerURIorFILE ?? ''); + if ($issuerURIorFILE === '' || $issuerURIorFILE === '0') { + $this->logger?->debug('Failed!'); + } else { + $this->logger?->debug(sprintf('OK got address "%s"...', $issuerURIorFILE)); + $this->logger?->debug(sprintf(' load issuer from "%s"...', $issuerURIorFILE)); + if ($issuerCert = @file_get_contents($issuerURIorFILE)) { + $this->logger?->debug('OK. size ' . round(strlen($issuerCert) / 1024, 2) . 'Kb'); + $this->logger?->debug(' reading issuer certificate...'); + if ($issuer_certDER = x509::get_cert($issuerCert)) { + $this->logger?->debug('OK'); + $this->logger?->debug(' check if issuer is cert issuer...'); + $certIssuer_parse = x509::readcert($issuer_certDER, 'oid'); // Parsing Issuer cert + $certSigner_signatureField = $certSigner_parse['signatureValue']; + if (openssl_public_decrypt(hex2bin((string) $certSigner_signatureField), $decrypted, x509::x509_der2pem($issuer_certDER), OPENSSL_PKCS1_PADDING)) { + $this->logger?->debug('OK issuer is cert issuer.'); + $ltvResult['issuer'] = $issuer_certDER; + } else { + $this->logger?->warning('FAILED! issuer is not cert issuer.'); + } + } else { + $this->logger?->warning('FAILED!'); + } } else { - p_debug(" FAIL!"); + $this->logger?->warning('FAILED!.'); } - $i++; - } - } else { - p_error(" FAILED! no extracerts available"); } - } - - } - - if($ltvResult['issuer']) { - if(!empty($ocspURI)) { - p_debug(" OCSP start..."); - $ocspReq_serialNumber = $certSigner_parse['tbsCertificate']['serialNumber']; - $ocspReq_issuerNameHash = $certIssuer_parse['tbsCertificate']['subject']['sha1']; - $ocspReq_issuerKeyHash = $certIssuer_parse['tbsCertificate']['subjectPublicKeyInfo']['sha1']; - $ocspRequestorSubjName = $certSigner_parse['tbsCertificate']['subject']['hexdump']; - p_debug(" OCSP create request..."); - if($ocspReq = x509::ocsp_request($ocspReq_serialNumber, $ocspReq_issuerNameHash, $ocspReq_issuerKeyHash)) { - p_debug(" OK."); - $ocspBinReq = pack("H*", $ocspReq); - $reqData = array( - 'data'=>$ocspBinReq, - 'uri'=>$ocspURI, - 'req_contentType'=>'application/ocsp-request', - 'resp_contentType'=>'application/ocsp-response' - ); - p_debug(" OCSP send request to \"$ocspURI\"..."); - if($ocspResp = self::sendReq($reqData)) { - p_debug(" OK."); - p_debug(" OCSP parsing response..."); - if($ocsp_parse = x509::ocsp_response_parse($ocspResp, $return)) { - p_debug(" OK."); - p_debug(" OCSP check cert validity..."); - $certStatus = $ocsp_parse['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responses'][0]['certStatus']; - if($certStatus == 'valid') { - p_debug(" OK. VALID."); - $ocspRespHex = $ocsp_parse['hexdump']; - $ltvResult['ocsp'] = $ocspRespHex; - } else { - p_warning(" FAILED! cert not valid, status:\"".strtoupper($certStatus)."\""); - } + + if (! $ltvResult['issuer']) { + $this->logger?->debug(' search for issuer in extracerts.....'); + if (array_key_exists('extracerts', $this->signature_data) && $this->signature_data['extracerts'] !== null && count($this->signature_data['extracerts']) > 0) { + $i = 0; + foreach ($this->signature_data['extracerts'] as $extracert) { + $this->logger?->debug(sprintf('extracerts[%d] ...', $i)); + $certSigner_signatureField = $certSigner_parse['signatureValue']; + if (openssl_public_decrypt(hex2bin((string) $certSigner_signatureField), $decrypted, $extracert, OPENSSL_PKCS1_PADDING)) { + $this->logger?->debug(' OK got issuer.'); + $certIssuer_parse = x509::readcert($extracert, 'oid'); // Parsing Issuer cert + $ltvResult['issuer'] = x509::get_cert($extracert); + } else { + $this->logger?->debug(' FAIL!'); + } + + $i++; + } } else { - p_warning(" FAILED! Ocsp server status \"$return\""); + throw new PDFException('FAILED! no extracerts available'); } - } else { - p_warning(" FAILED!"); - } - } else { - p_warning(" FAILED!"); } - } - - if(!$ltvResult['ocsp']) {// CRL not processed if OCSP validation already success - if(!empty($crlURIorFILE)) { - p_debug(" processing CRL validation since OCSP not done/failed..."); - p_debug(" getting crl from \"$crlURIorFILE\"..."); - if($crl = @file_get_contents($crlURIorFILE)) { - p_debug(" OK. size ".round(strlen($crl)/1024,2)."Kb"); - p_debug(" reading crl..."); - if($crlread=x509::crl_read($crl)) { - p_debug(" OK"); - p_debug(" verify crl signature..."); - $crl_signatureField = $crlread['parse']['signature']; - if(openssl_public_decrypt(hex2bin($crl_signatureField), $decrypted, x509::x509_der2pem($ltvResult['issuer']), OPENSSL_PKCS1_PADDING)) { - p_debug(" OK"); - p_debug(" check CRL validity..."); - $crl_parse=$crlread['parse']; - $thisUpdate = str_pad($crl_parse['TBSCertList']['thisUpdate'], 15, "20", STR_PAD_LEFT); - $thisUpdateTime = strtotime($thisUpdate); - $nextUpdate = str_pad($crl_parse['TBSCertList']['nextUpdate'], 15, "20", STR_PAD_LEFT); - $nextUpdateTime = strtotime($nextUpdate); - $nowz = strtotime("now"); - if(($nowz-$thisUpdateTime) < 0) { // 0 sec after valid - p_error(" FAILED! not yet valid! valid at ".date("d/m/Y H:i:s", $thisUpdateTime)); - } elseif(($nextUpdateTime-$nowz) < 1) { // not accept if crl 1 sec remain to expired - p_error(" FAILED! Expired crl at ".date("d/m/Y H:i:s", $nextUpdateTime)." and now ".date("d/m/Y H:i:s", $nowz)."!"); + + if ($ltvResult['issuer']) { + if ($ocspURI !== '' && $ocspURI !== '0') { + $this->logger?->debug(' OCSP start...'); + $ocspReq_serialNumber = $certSigner_parse['tbsCertificate']['serialNumber']; + $ocspReq_issuerNameHash = $certIssuer_parse['tbsCertificate']['subject']['sha1']; + $ocspReq_issuerKeyHash = $certIssuer_parse['tbsCertificate']['subjectPublicKeyInfo']['sha1']; + $this->logger?->debug(' OCSP create request...'); + if ($ocspReq = x509::ocsp_request($ocspReq_serialNumber, $ocspReq_issuerNameHash, $ocspReq_issuerKeyHash)) { + $this->logger?->debug('OK.'); + $ocspBinReq = pack('H*', $ocspReq); + $reqData = [ + 'data' => $ocspBinReq, + 'uri' => $ocspURI, + 'req_contentType' => 'application/ocsp-request', + 'resp_contentType' => 'application/ocsp-response', + ]; + $this->logger?->debug(sprintf(' OCSP send request to "%s"...', $ocspURI)); + if (($ocspResp = $this->sendReq($reqData)) !== '' && ($ocspResp = $this->sendReq($reqData)) !== '0') { + $this->logger?->debug('OK.'); + $this->logger?->debug(' OCSP parsing response...'); + if ($ocsp_parse = x509::ocsp_response_parse($ocspResp, $return)) { + $this->logger?->debug('OK.'); + $this->logger?->debug(' OCSP check cert validity...'); + $certStatus = $ocsp_parse['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responses'][0]['certStatus']; + if ($certStatus === 'valid') { + $this->logger?->debug('OK. VALID.'); + $ocspRespHex = $ocsp_parse['hexdump']; + $ltvResult['ocsp'] = $ocspRespHex; + } else { + $this->logger?->warning('FAILED! cert not valid, status:"' . strtoupper((string) $certStatus) . '"'); + } + } else { + $this->logger?->warning(sprintf('FAILED! Ocsp server status "%s"', $return)); + } + } else { + $this->logger?->warning('FAILED!'); + } } else { - p_debug(" OK CRL still valid until ".date("d/m/Y H:i:s", $nextUpdateTime).""); - $crlCertValid=true; - p_debug(" check if cert not revoked..."); - if(array_key_exists('revokedCertificates', $crl_parse['TBSCertList'])) { - $certSigner_serialNumber = $certSigner_parse['tbsCertificate']['serialNumber']; - if(array_key_exists($certSigner_serialNumber, $crl_parse['TBSCertList']['revokedCertificates']['lists'])) { - $crlCertValid=false; - p_error(" FAILED! Certificate Revoked!"); + $this->logger?->warning(' FAILED!'); + } + } + + // CRL not processed if OCSP validation already success + if (! $ltvResult['ocsp'] && ! empty($crlURIorFILE)) { + $this->logger?->debug(' processing CRL validation since OCSP not done/failed...'); + $this->logger?->debug(sprintf(' getting crl from "%s"...', $crlURIorFILE)); + if ($crl = @file_get_contents($crlURIorFILE)) { + $this->logger?->debug('OK. size ' . round(strlen($crl) / 1024, 2) . 'Kb'); + $this->logger?->debug(' reading crl...'); + if ($crlread = x509::crl_read($crl)) { + $this->logger?->debug('OK'); + $this->logger?->debug(' verify crl signature...'); + $crl_signatureField = $crlread['parse']['signature']; + if (openssl_public_decrypt(hex2bin((string) $crl_signatureField), $decrypted, x509::x509_der2pem($ltvResult['issuer']), OPENSSL_PKCS1_PADDING)) { + $this->logger?->debug('OK'); + $this->logger?->debug(' check CRL validity...'); + $crl_parse = $crlread['parse']; + $thisUpdate = str_pad((string) $crl_parse['TBSCertList']['thisUpdate'], 15, '20', STR_PAD_LEFT); + $thisUpdateTime = strtotime($thisUpdate); + $nextUpdate = str_pad((string) $crl_parse['TBSCertList']['nextUpdate'], 15, '20', STR_PAD_LEFT); + $nextUpdateTime = strtotime($nextUpdate); + $nowz = time(); + if ($nowz - $thisUpdateTime < 0) { // 0 sec after valid + throw new PDFException('FAILED! not yet valid! valid at ' . date('d/m/Y H:i:s', $thisUpdateTime)); + } + + if ($nextUpdateTime - $nowz < 1) { // not accept if crl 1 sec remain to expired + throw new PDFException('FAILED! Expired crl at ' . date('d/m/Y H:i:s', $nextUpdateTime) . ' and now ' . date('d/m/Y H:i:s', $nowz) . '!'); + } + + $this->logger?->debug('OK CRL still valid until ' . date('d/m/Y H:i:s', $nextUpdateTime)); + $crlCertValid = true; + $this->logger?->debug(' check if cert not revoked...'); + if (array_key_exists('revokedCertificates', $crl_parse['TBSCertList'])) { + $certSigner_serialNumber = $certSigner_parse['tbsCertificate']['serialNumber']; + if (array_key_exists($certSigner_serialNumber, $crl_parse['TBSCertList']['revokedCertificates']['lists'])) { + throw new PDFException('FAILED! Certificate Revoked!'); + } + } + + if ($crlCertValid) { + $this->logger?->debug('OK. VALID'); + $crlHex = current(unpack('H*', (string) $crlread['der'])); + $ltvResult['crl'] = $crlHex; + } + } else { + throw new PDFException('FAILED! Wrong CRL.'); + } + } else { + throw new PDFException("FAILED! can't read crl"); } - } - if($crlCertValid == true) { - p_debug(" OK. VALID"); - $crlHex = current(unpack('H*', $crlread['der'])); - $ltvResult['crl'] = $crlHex; - } + } else { + throw new PDFException("FAILED! can't get crl"); } - } else { - p_error(" FAILED! Wrong CRL."); - } - } else { - p_error(" FAILED! can't read crl"); } - } else { - p_error(" FAILED! can't get crl"); - } } - } - } - if(!$ltvResult['issuer']) { - return false; - } - if(!$ltvResult['ocsp'] && !$ltvResult['crl']) { - return false; - } - return $ltvResult; - } - - /** - * Perform PKCS7 Signing - * @param string $binaryData - * @return string hex + padding 0 - * @public - */ - public function pkcs7_sign($binaryData) { - $hexOidHashAlgos = array( - 'md2'=>'06082A864886F70D0202', - 'md4'=>'06082A864886F70D0204', - 'md5'=>'06082A864886F70D0205', - 'sha1'=>'06052B0E03021A', - 'sha224'=>'0609608648016503040204', - 'sha256'=>'0609608648016503040201', - 'sha384'=>'0609608648016503040202', - 'sha512'=>'0609608648016503040203' - ); - $hashAlgorithm = $this->signature_data['hashAlgorithm']; - if(!array_key_exists($hashAlgorithm, $hexOidHashAlgos)) { - p_error("not support hash algorithm!"); - return false; - } - p_debug("hash algorithm is \"$hashAlgorithm\""); - $x509 = new x509; - if(!$certParse = $x509->readcert($this->signature_data['signcert'])) { - p_error("certificate error! check certificate"); + + if (! $ltvResult['issuer']) { + return false; + } + + if (! $ltvResult['ocsp'] && ! $ltvResult['crl']) { + return false; + } + + return $ltvResult; } - $hexEmbedCerts[] = bin2hex($x509->get_cert($this->signature_data['signcert'])); - $appendLTV = ''; - $ltvData = $this->signature_data['ltv']; - if(!empty($ltvData)) { - p_debug(" LTV Validation start..."); - $appendLTV = ''; - $LTVvalidation_ocsp = ''; - $LTVvalidation_crl = ''; - $LTVvalidation_issuer = ''; - $LTVvalidationEnd = false; - - $isRootCA = false; - if($certParse['tbsCertificate']['issuer']['hexdump'] == $certParse['tbsCertificate']['subject']['hexdump']) { // check whether root ca - if(openssl_public_decrypt(hex2bin($certParse['signatureValue']), $decrypted, x509::x509_der2pem($x509->get_cert($this->signature_data['signcert'])), OPENSSL_PKCS1_PADDING)) { - p_debug("***** \"{$certParse['tbsCertificate']['subject']['2.5.4.3'][0]}\" is a ROOT CA. No validation performed ***"); - $isRootCA = true; + + /** + * parse tsa response to array + * + * @param string $binaryTsaRespData binary tsa response to parse + * + * @return array asn.1 hex structure of tsa response + */ + private function tsa_parseResp(string $binaryTsaRespData) + { + if (! @$ar = asn1::parse(bin2hex($binaryTsaRespData), 3)) { + throw new PDFException("can't parse invalid tsa Response."); } - } - if($isRootCA == false) { - $i = 0; - $LTVvalidation = true; - $certtoCheck = $certParse; - while($LTVvalidation !== false) { - p_debug("========= $i checking \"{$certtoCheck['tbsCertificate']['subject']['2.5.4.3'][0]}\"==============="); - $LTVvalidation = self::LTVvalidation($certtoCheck); - $i++; - if($LTVvalidation) { - $curr_issuer = $LTVvalidation['issuer']; - $certtoCheck = $x509->readcert($curr_issuer, 'oid'); - if(@$LTVvalidation['ocsp'] || @$LTVvalidation['crl']) { - $LTVvalidation_ocsp .= $LTVvalidation['ocsp']; - $LTVvalidation_crl .= $LTVvalidation['crl']; - $hexEmbedCerts[] = bin2hex($LTVvalidation['issuer']); + + $curr = $ar; + foreach ($curr as $key => $value) { + if ($value['type'] == '30') { + $curr['TimeStampResp'] = $curr[$key]; + unset($curr[$key]); } - - if($certtoCheck['tbsCertificate']['issuer']['hexdump'] == $certtoCheck['tbsCertificate']['subject']['hexdump']) { // check whether root ca - if(openssl_public_decrypt(hex2bin($certtoCheck['signatureValue']), $decrypted, $x509->x509_der2pem($curr_issuer), OPENSSL_PKCS1_PADDING)) { - p_debug("========= FINISH Reached ROOT CA \"{$certtoCheck['tbsCertificate']['subject']['2.5.4.3'][0]}\"==============="); - $LTVvalidationEnd = true; - break; - } + } + + $ar = $curr; + $curr = $ar['TimeStampResp']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30' && ! array_key_exists('status', $curr)) { + $curr['status'] = $curr[$key]; + unset($curr[$key]); + } elseif ($value['type'] == '30') { + $curr['timeStampToken'] = $curr[$key]; + unset($curr[$key]); + } } - } } - - if($LTVvalidationEnd) { - p_debug(" LTV Validation SUCCESS\n"); - $ocsp = ''; - if(!empty($LTVvalidation_ocsp)) { - $ocsp = asn1::expl(1, - asn1::seq( - $LTVvalidation_ocsp - ) - ); - } - $crl = ''; - if(!empty($LTVvalidation_crl)) { - $crl = asn1::expl(0, - asn1::seq( - $LTVvalidation_crl - ) - ); - } - $appendLTV = asn1::seq( - "06092A864886F72F010108". // adbe-revocationInfoArchival (1.2.840.113583.1.1.8) - asn1::set( - asn1::seq( - $ocsp. - $crl - ) - ) - ); - } else { - p_warning(" LTV Validation FAILED!\n"); + + $ar['TimeStampResp'] = $curr; + $curr = $ar['TimeStampResp']['timeStampToken']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '06') { + $curr['contentType'] = $curr[$key]; + unset($curr[$key]); + } + + if ($value['type'] === 'a0') { + $curr['content'] = $curr[$key]; + unset($curr[$key]); + } + } } - } - foreach($this->signature_data['extracerts'] ?? [] as $extracert) { - $hex_extracert = bin2hex($x509->x509_pem2der($extracert)); - if(!in_array($hex_extracert, $hexEmbedCerts)) { - $hexEmbedCerts[] = $hex_extracert; + + $ar['TimeStampResp']['timeStampToken'] = $curr; + $curr = $ar['TimeStampResp']['timeStampToken']['content']; + foreach ($curr as $key => $value) { + if (is_numeric($key) && $value['type'] == '30') { + $curr['TSTInfo'] = $curr[$key]; + unset($curr[$key]); + } } - } - } - $messageDigest = hash($hashAlgorithm, $binaryData); - $authenticatedAttributes= asn1::seq( - '06092A864886F70D010903'. //OBJ_pkcs9_contentType 1.2.840.113549.1.9.3 - asn1::set('06092A864886F70D010701') //OBJ_pkcs7_data 1.2.840.113549.1.7.1 - ). - asn1::seq( // signing time - '06092A864886F70D010905'. //OBJ_pkcs9_signingTime 1.2.840.113549.1.9.5 - asn1::set( - asn1::utime(date("ymdHis")) //UTTC Time - ) - ). - asn1::seq( // messageDigest - '06092A864886F70D010904'. //OBJ_pkcs9_messageDigest 1.2.840.113549.1.9.4 - asn1::set(asn1::oct($messageDigest)) - ). - $appendLTV; - $tohash = asn1::set($authenticatedAttributes); - $hash = hash($hashAlgorithm, hex2bin($tohash)); - $toencrypt = asn1::seq( - asn1::seq($hexOidHashAlgos[$hashAlgorithm]."0500"). // OBJ $messageDigest & OBJ_null - asn1::oct($hash) - ); - $pkey = $this->signature_data['privkey']; - if(!openssl_private_encrypt(hex2bin($toencrypt), $encryptedDigest, $pkey, OPENSSL_PKCS1_PADDING)) { - p_error("openssl_private_encrypt error! can't encrypt"); - return false; - } - $hexencryptedDigest = bin2hex($encryptedDigest); - $timeStamp = ''; - if(!empty($this->signature_data['tsa'])) { - p_debug(" Timestamping process start..."); - if($TSTInfo = self::createTimestamp($encryptedDigest, $hashAlgorithm)) { - p_debug(" Timestamping SUCCESS."); - $TimeStampToken = asn1::seq( - "060B2A864886F70D010910020E". // OBJ_id_smime_aa_timeStampToken 1.2.840.113549.1.9.16.2.14 - asn1::set($TSTInfo) - ); - $timeStamp = asn1::expl(1,$TimeStampToken); - } else { - p_warning(" Timestamping FAILED!"); - } + + $ar['TimeStampResp']['timeStampToken']['content'] = $curr; + if (@$ar['TimeStampResp']['timeStampToken']['content']['hexdump'] != '') { + return $ar; + } + + return false; } - $issuerName = $certParse['tbsCertificate']['issuer']['hexdump']; - $serialNumber = $certParse['tbsCertificate']['serialNumber']; - $signerinfos = asn1::seq( - asn1::int('1'). - asn1::seq($issuerName . asn1::int($serialNumber)). - asn1::seq($hexOidHashAlgos[$hashAlgorithm].'0500'). - asn1::expl(0, $authenticatedAttributes). - asn1::seq( - '06092A864886F70D010101'. //OBJ_rsaEncryption - '0500' - ). - asn1::oct($hexencryptedDigest). - $timeStamp - ); - $certs = asn1::expl(0,implode('', $hexEmbedCerts)); - $pkcs7contentSignedData = asn1::seq( - asn1::int('1'). - asn1::set(asn1::seq($hexOidHashAlgos[$hashAlgorithm].'0500')). - asn1::seq('06092A864886F70D010701'). //OBJ_pkcs7_data - $certs. - asn1::set($signerinfos) - ); - $pkcs7ContentInfo = asn1::seq( - "06092A864886F70D010702". // Hexadecimal form of pkcs7-signedData - asn1::expl(0,$pkcs7contentSignedData) - ); - return $pkcs7ContentInfo; - } } diff --git a/src/helpers/DependencyTreeObject.php b/src/helpers/DependencyTreeObject.php index d611ed5..3ca346a 100644 --- a/src/helpers/DependencyTreeObject.php +++ b/src/helpers/DependencyTreeObject.php @@ -20,119 +20,116 @@ */ namespace ddn\sapp\helpers; -use function ddn\sapp\helpers\p_warning; + +use ddn\sapp\PDFObject; +use ddn\sapp\pdfvalue\PDFValueObject; +use Generator; +use Stringable; /** * A class for the PDFObjects in the dependency tree */ -class DependencyTreeObject { - function __construct($oid, $info = null) { - $this->info = $info; - $this->is_child = 0; - $this->oid = $oid; +class DependencyTreeObject implements Stringable +{ + public int $is_child = 0; + + private array $children = []; + + public function __construct( + private int $oid, + public mixed $info = null + ) { + } + + public function __toString(): string + { + return $this->_getstr(null, isset($this->children) ? count($this->children) : 0); } /** * Function that links one object to its parent (i.e. adds the object to the list of children of this object) * - the function increases the amount of times that one object has been added to a parent object, to detect problems in building the tree */ - function addchild($oid, $o) { - if (!isset($this->children)) $this->children = []; + public function addchild(int $oid, self $o): void + { $this->children[$oid] = $o; - if ($o->is_child != 0) - p_warning("object $o->oid is already a child of other object"); - $o->is_child = $o->is_child + 1; + $o->is_child++; } /** * This is an iterator for the children of this object */ - function children() { - if (isset($this->children)) - foreach ($this->children as $oid => $object) { + public function children(): Generator + { + if (isset($this->children)) { + foreach (array_keys($this->children) as $oid) { yield $oid; } + } } /** * Gets a string that represents the object, prepending a number of spaces, proportional to the depth in the tree */ - protected function _getstr($spaces = "", $mychcount = 0) { + protected function _getstr(?string $spaces = '', int $mychcount = 0): string + { // $info = $this->oid . ($this->info?" ($this->info)":"") . (($this->is_child > 1)?" $this->is_child":""); - $info = $this->oid . ($this->info?" ($this->info)":""); + $info = $this->oid . ($this->info !== null ? sprintf(' (%s)', $this->info) : ''); if ($spaces === null) { - $lines = [ "{$spaces} " . json_decode('"\u2501"') . " $info" ]; - } else - if ($mychcount == 0) - $lines = [ "{$spaces} " . json_decode('"\u2514\u2500"') . " $info" ]; - else - $lines = [ "{$spaces} " . json_decode('"\u251c\u2500"') . " $info" ]; + $lines = [$spaces . ' ' . json_decode('"\u2501"', false, 512, JSON_THROW_ON_ERROR) . ' ' . $info]; + } elseif ($mychcount === 0) { + $lines = [$spaces . ' ' . json_decode('"\u2514\u2500"', false, 512, JSON_THROW_ON_ERROR) . ' ' . $info]; + } else { + $lines = [$spaces . ' ' . json_decode('"\u251c\u2500"', false, 512, JSON_THROW_ON_ERROR) . ' ' . $info]; + } + if (isset($this->children)) { $chcount = count($this->children); - foreach ($this->children as $oid => $child) { + foreach ($this->children as $child) { $chcount--; - if (($spaces === null) || ($mychcount == 0)) { - array_push($lines, $child->_getstr($spaces . " " , $chcount)); - } else - array_push($lines, $child->_getstr($spaces . " " . json_decode('"\u2502"'), $chcount)); + if ($spaces === null || $mychcount === 0) { + $lines[] = $child->_getstr($spaces . ' ', $chcount); + } else { + $lines[] = $child->_getstr($spaces . ' ' . json_decode('"\u2502"', false, 512, JSON_THROW_ON_ERROR), $chcount); + } } } - return implode("\n", $lines); - } - protected function _old_getstr($depth = 0) { - $spaces = str_repeat(" " . json_decode('"\u2502"'), $depth); - $lines = [ "{$spaces} " . json_decode('"\u251c\u2500"') ." " . $this->oid . ($this->info?" ($this->info)":"") . (($this->is_child > 1)?" $this->is_child":"") ]; - if (isset($this->children)) { - foreach ($this->children as $oid => $child) { - array_push($lines, $child->_getstr($depth + 1)); - } - } return implode("\n", $lines); } - - public function __toString() { - return $this->_getstr(null, isset($this->children)?count($this->children):0); - } } - -/** - * Fields that are blacklisted for referencing the fields; - * i.e. a if a reference to a object appears in a fields in the blacklist, it won't be considered as a reference to other object to build the tree - * +/** + * Fields that are blacklisted for referencing the fields; + * i.e. a if a reference to a object appears in a fields in the blacklist, it won't be considered as a reference to other object to build the tree * The blacklist is indexed by the type of the node; * means "any type" (including the others in the blacklist) */ const BLACKLIST = [ // Field "Parent" for any type of object - "*" => [ "Parent" ], + '*' => ['Parent'], // Field "P" for nodes of type "Annot" - "Annot" => [ "P" ] + 'Annot' => ['P'], ]; -function references_in_object($object, $oid = false) { - $type = $object["Type"]; - if ($type !== false) - $type = $type->val(); - else - $type = ""; +function references_in_object(PDFObject $object): array +{ + $type = $object['Type']; + $type = $type !== false ? $type->val() : ''; $references = []; foreach ($object->get_keys() as $key) { - $valid = true; - // We'll skip those blacklisted fields - if (in_array($key, BLACKLIST["*"])) + if (in_array($key, BLACKLIST['*'], true)) { continue; + } - if (array_key_exists($type, BLACKLIST)) - if (in_array($key, BLACKLIST[$type])) - continue; + if (array_key_exists($type, BLACKLIST) && in_array($key, BLACKLIST[$type], true)) { + continue; + } - $r_objects = []; - if (is_a($object[$key], "ddn\\sapp\\pdfvalue\\PDFValueObject")) { + if ($object[$key] instanceof PDFValueObject) { $r_objects = references_in_object($object[$key]); } else { // Function get_object_referenced checks whether the value (or values in a list) have the form of object references, and if they have the form @@ -140,11 +137,15 @@ function references_in_object($object, $oid = false) { $r_objects = $object[$key]->get_object_referenced(); // If the value does not have the form of a reference, it returns false - if ($r_objects === false) + if ($r_objects === false) { continue; - - if (!is_array($r_objects)) $r_objects = [ $r_objects ]; + } + + if (! is_array($r_objects)) { + $r_objects = [$r_objects]; + } } + // p_debug($key . "=>" . implode(",",$r_objects)); array_push($references, ...$r_objects); @@ -152,4 +153,4 @@ function references_in_object($object, $oid = false) { // Return the list of references in the fields of the object return $references; -} \ No newline at end of file +} diff --git a/src/helpers/LoadHelpers.php b/src/helpers/LoadHelpers.php index a535a1c..e1ef4d0 100644 --- a/src/helpers/LoadHelpers.php +++ b/src/helpers/LoadHelpers.php @@ -2,8 +2,10 @@ namespace ddn\sapp\helpers; -foreach (glob(__DIR__ . "/*.php") as $i) { - include_once($i); +foreach (glob(__DIR__ . '/*.php') as $i) { + include_once $i; } -class LoadHelpers {} \ No newline at end of file +class LoadHelpers +{ +} diff --git a/src/helpers/StreamReader.php b/src/helpers/StreamReader.php index de83a1b..50eabba 100644 --- a/src/helpers/StreamReader.php +++ b/src/helpers/StreamReader.php @@ -25,21 +25,24 @@ * This class abstracts the reading from a stream of data (i.e. a string). The objective of * using this class is to enable the creation of other classes (e.g. FileStreamReader) to * read from other char streams (e.g. a file) - * * The class gets a string that will be used as the buffer to read, and then it is possible * to sequentially get the characters from the string using funcion *nextchar*, that will * return "false" when the stream is finished. - * * Other functions to change the position are also available (e.g. goto) */ -class StreamReader { - protected $_buffer = ""; - protected $_bufferlen = 0; +class StreamReader +{ + protected ?string $_buffer; + + protected int $_bufferlen; + protected $_pos = 0; - public function __construct($string = null, $offset = 0) { - if ($string === null) - $string = ""; + public function __construct(?string $string = null, int $offset = 0) + { + if ($string === null) { + $string = ''; + } $this->_buffer = $string; $this->_bufferlen = strlen($string); @@ -48,78 +51,99 @@ public function __construct($string = null, $offset = 0) { /** * Advances the buffer to the next char and returns it + * * @return char the next char in the buffer - * */ - public function nextchar() { + public function nextchar(): string|false + { $this->_pos = min($this->_pos + 1, $this->_bufferlen); + return $this->currentchar(); } /** * Advances the buffer to the next n chars and returns them + * * @param n the number of chars to read + * * @return str the substring obtained (with at most, n chars) */ - public function nextchars($n) { + public function nextchars($n): string + { $n = min($n, $this->_bufferlen - $this->_pos); - $retval = substr($this->_buffer, $this->_pos, $n); + $retval = substr((string) $this->_buffer, $this->_pos, $n); $this->_pos += $n; + return $retval; } /** * Returns the current char + * * @return char the current char */ - public function currentchar() { - if ($this->_pos >= $this->_bufferlen) + public function currentchar(): string|false + { + if ($this->_pos >= $this->_bufferlen) { return false; + } return $this->_buffer[$this->_pos]; } /** * Returns whether the stream has finished or not + * * @return finished true if there are no more chars to read from the stream; false otherwise */ - public function eos() { + public function eos(): bool + { return $this->_pos >= $this->_bufferlen; } /** * Sets the position of the buffer to the position in the parameter + * * @param pos the position to which the buffer must be set */ - public function goto($pos = 0) { + public function goto($pos = 0): void + { $this->_pos = min(max(0, $pos), $this->_bufferlen); } /** * Obtains a substring that begins at current position. + * * @param length length of the substring to obtain (0 or <0 will obtain the whole buffer from the current position) + * * @return substr the substring */ - public function substratpos($length = 0) { - if ($length > 0) - return substr($this->_buffer, $this->_pos, $length); - else - return substr($this->_buffer, $this->_pos); + public function substratpos($length = 0): string + { + if ($length > 0) { + return substr((string) $this->_buffer, $this->_pos, $length); + } + + return substr((string) $this->_buffer, $this->_pos); } /** * Gets the current position of the buffer + * * @return position the position of the buffer */ - public function getpos() { + public function getpos() + { return $this->_pos; } /** * Obtains the size of the buffer + * * @return size the size of the buffer */ - public function size() { + public function size(): int + { return $this->_bufferlen; } } diff --git a/src/helpers/UUID.php b/src/helpers/UUID.php index 7810793..327e48d 100644 --- a/src/helpers/UUID.php +++ b/src/helpers/UUID.php @@ -1,110 +1,128 @@ "ASN1_EOC", - "01" => "ASN1_BOOLEAN", - "02" => "ASN1_INTEGER", - "03" => "ASN1_BIT_STRING", - "04" => "ASN1_OCTET_STRING", - "05" => "ASN1_NULL", - "06" => "ASN1_OBJECT", - "07" => "ASN1_OBJECT_DESCRIPTOR", - "08" => "ASN1_EXTERNAL", - "09" => "ASN1_REAL", - "0a" => "ASN1_ENUMERATED", - "0c" => "ASN1_UTF8STRING", - "30" => "ASN1_SEQUENCE", - "31" => "ASN1_SET", - "12" => "ASN1_NUMERICSTRING", - "13" => "ASN1_PRINTABLESTRING", - "14" => "ASN1_T61STRING", - "15" => "ASN1_VIDEOTEXSTRING", - "16" => "ASN1_IA5STRING", - "17" => "ASN1_UTCTIME", - "18" => "ASN1_GENERALIZEDTIME", - "19" => "ASN1_GRAPHICSTRING", - "1a" => "ASN1_VISIBLESTRING", - "1b" => "ASN1_GENERALSTRING", - "1c" => "ASN1_UNIVERSALSTRING", - "1d" => "ASN1_BMPSTRING" - ); - return array_key_exists($id,$asn1_Types)?$asn1_Types[$id]:$id; - } - - /** - * parse asn.1 to array - * to be called from parse() function - * @param string $hex asn.1 hex form - * @return array asn.1 structure - */ - protected static function oneParse($hex) { - if($hex == '') { - return false; - } - if(!@ctype_xdigit($hex) || @strlen($hex)%2!=0) { - echo "input:\"$hex\" not hex string!.\n"; - return false; - } - $stop = false; - while($stop == false) { - $asn1_type = substr($hex, 0, 2); - $tlv_tagLength = hexdec(substr($hex, 2, 2)); - if($tlv_tagLength > 127) { - $tlv_lengthLength = $tlv_tagLength-128; - $tlv_valueLength = substr($hex, 4, ($tlv_lengthLength*2)); - } else { - $tlv_lengthLength = 0; - $tlv_valueLength = substr($hex, 2, 2+($tlv_lengthLength*2)); - } - if($tlv_lengthLength >4) { // limit tlv_lengthLength to FFFF - return false; - } - $tlv_valueLength = hexdec($tlv_valueLength); - $totalTlLength = 2+2+($tlv_lengthLength*2); - $reduction = 2+2+($tlv_lengthLength*2)+($tlv_valueLength*2); - $tlv_value = substr($hex, $totalTlLength, $tlv_valueLength*2); - $remain = substr($hex, $totalTlLength+($tlv_valueLength*2)); - $newhexdump = substr($hex, 0, $totalTlLength+($tlv_valueLength*2)); - $result[] = array( - 'tlv_tagLength'=>strlen(dechex($tlv_tagLength))%2==0?dechex($tlv_tagLength):'0'.dechex($tlv_tagLength), - 'tlv_lengthLength'=>$tlv_lengthLength, - 'tlv_valueLength'=>$tlv_valueLength, - 'newhexdump'=>$newhexdump, - 'typ'=>$asn1_type, - 'tlv_value'=>$tlv_value - ); - if($remain == '') { // if remains string was empty & contents also empty, function return FALSE - $stop = true; - } else { - $hex = $remain; - } - } - return $result; - } - - /** - * parse asn.1 to array recursively - * @param string $hex asn.1 hex form - * @param int $maxDepth maximum parsing depth - * @return array asn.1 structure recursively to specific depth - */ - public static function parse($hex, $maxDepth=5) { - $result = array(); - static $currentDepth = 0; - if($asn1parse_array = self::oneParse($hex)) { - foreach($asn1parse_array as $ff){ - $parse_recursive = false; - unset($info); - $k = $ff['typ']; - $v = $ff['tlv_value']; - $info['depth']=$currentDepth; - $info['hexdump']=$ff['newhexdump']; - $info['type'] = $k; - $info['typeName'] = self::type($k); - $info['value_hex'] = $v; - if(($currentDepth <= $maxDepth)) { - if($k == '06') { - - } else if(in_array($k, ['13', '18'])) { - $info['value'] = hex2bin($info['value_hex']); - } else if(in_array($k, ['03', '02', 'a04'])) { - $info['value'] = $v; - } else { - $currentDepth++; - $parse_recursive = self::parse($v, $maxDepth); - $currentDepth--; - } - if($parse_recursive) { - $result[] = array_merge($info, $parse_recursive); - } else { - $result[] = $info; - } +class asn1 +{ + public static function __callStatic($func, $params) + { + $func = strtolower((string) $func); + $asn1Tag = self::asn1Tag($func); + if ($asn1Tag !== false) { + $num = $asn1Tag; //value of array + $hex = $params[0]; + $val = $hex; + if (in_array($func, ['printable', 'utf8', 'ia5', 'visible', 't61'], true)) { // ($string) + $val = bin2hex((string) $hex); + } + + if ($func === 'int') { + $val = strlen((string) $val) % 2 !== 0 ? '0' . $val : (string) $val; + } + + if ($func === 'expl') { //expl($num, $hex) + $num .= $params[0]; + $val = $params[1]; + } + + if ($func === 'impl') { //impl($num="0") + $val ??= '00'; + $val = strlen((string) $val) % 2 !== 0 ? '0' . $val : $val; + + return $num . $val; + } + + if ($func === 'other') { //OTHER($id, $hex, $chr = false) + $id = $params[0]; + $hex = $params[1]; + $chr = @$params[2]; + $str = $hex; + if ($chr) { + $str = bin2hex((string) $hex); + } + + return $id . self::asn1_header($str) . $str; + } + + if ($func === 'utime') { + $time = $params[0]; //yymmddhhiiss + $oldTz = date_default_timezone_get(); + date_default_timezone_set('UTC'); + $time = date('ymdHis', $time); + date_default_timezone_set($oldTz); + $val = bin2hex($time . 'Z'); + } + + if ($func === 'gtime') { + if (! $time = strtotime((string) $params[0])) { + // echo "asn1::GTIME function strtotime cant recognize time!! please check at input=\"{$params[0]}\""; + return false; + } + + $oldTz = date_default_timezone_get(); + // date_default_timezone_set("UTC"); + $time = date('YmdHis', $time); + date_default_timezone_set($oldTz); + $val = bin2hex($time . 'Z'); + } + + $hdr = self::asn1_header($val); + + return $num . $hdr . $val; } - } - } - return $result; - } - // =====End ASN.1 Parser section===== - - - // =====Begin ASN.1 Builder section===== - /** - * create asn.1 TLV tag length, length length and value length - * to be called from asn.1 builder functions - * @param string $str string value of asn.1 - * @return string hex of asn.1 TLV tag length - */ - protected static function asn1_header($str) { - $len = strlen($str)/2; - $ret = dechex($len); - if(strlen($ret)%2 != 0) { - $ret = "0$ret"; + + // echo "asn1 \"$func\" not exists!"; + return null; + } - $headerLength = strlen($ret)/2; - if($len > 127) { - $ret = "8".$headerLength.$ret; + + /** + * parse asn.1 to array recursively + * + * @param string $hex asn.1 hex form + * @param int $maxDepth maximum parsing depth + * + * @return array asn.1 structure recursively to specific depth + */ + public static function parse(string $hex, int $maxDepth = 5): array + { + $result = []; + static $currentDepth = 0; + if ($asn1parse_array = self::oneParse($hex)) { + foreach ($asn1parse_array as $ff) { + $parse_recursive = false; + unset($info); + $k = $ff['typ']; + $v = $ff['tlv_value']; + $info['depth'] = $currentDepth; + $info['hexdump'] = $ff['newhexdump']; + $info['type'] = $k; + $info['typeName'] = self::type($k); + $info['value_hex'] = $v; + if ($currentDepth <= $maxDepth) { + if ($k !== '06') { + if (in_array($k, ['13', '18'], true)) { + $info['value'] = hex2bin((string) $info['value_hex']); + } elseif (in_array($k, ['03', '02', 'a04'], true)) { + $info['value'] = $v; + } else { + $currentDepth++; + $parse_recursive = self::parse($v, $maxDepth); + $currentDepth--; + } + } + + $result[] = $parse_recursive ? array_merge($info, $parse_recursive) : $info; + } + } + } + + return $result; } - return $ret; - } - - /** - * create various dynamic function for asn1 - */ - private static function asn1Tag($name) { - $functionList = array( - 'seq'=>'30', - 'oct'=>'04', - 'obj'=>'06', - 'bit'=>'03', - 'printable'=>'13', - 'int'=>'02', - 'set'=>'31', - 'expl'=>'a', - 'utime'=>'17', - 'gtime'=>'18', - 'utf8'=>'0c', - 'ia5'=>'16', - 'visible'=>'1a', - 't61'=>'14', - 'impl'=>'80', - 'other'=>'' - ); - if(array_key_exists($name, $functionList)) { - return $functionList[$name]; - } else { - // echo "func \"$name\" not available"; - return false; + + // =====Begin ASN.1 Parser section===== + /** + * get asn.1 type tag name + * + * @param string $id hex asn.1 type tag + * + * @return string asn.1 tag name + */ + protected static function type(string $id): string + { + $asn1_Types = [ + '00' => 'ASN1_EOC', + '01' => 'ASN1_BOOLEAN', + '02' => 'ASN1_INTEGER', + '03' => 'ASN1_BIT_STRING', + '04' => 'ASN1_OCTET_STRING', + '05' => 'ASN1_NULL', + '06' => 'ASN1_OBJECT', + '07' => 'ASN1_OBJECT_DESCRIPTOR', + '08' => 'ASN1_EXTERNAL', + '09' => 'ASN1_REAL', + '0a' => 'ASN1_ENUMERATED', + '0c' => 'ASN1_UTF8STRING', + '30' => 'ASN1_SEQUENCE', + '31' => 'ASN1_SET', + '12' => 'ASN1_NUMERICSTRING', + '13' => 'ASN1_PRINTABLESTRING', + '14' => 'ASN1_T61STRING', + '15' => 'ASN1_VIDEOTEXSTRING', + '16' => 'ASN1_IA5STRING', + '17' => 'ASN1_UTCTIME', + '18' => 'ASN1_GENERALIZEDTIME', + '19' => 'ASN1_GRAPHICSTRING', + '1a' => 'ASN1_VISIBLESTRING', + '1b' => 'ASN1_GENERALSTRING', + '1c' => 'ASN1_UNIVERSALSTRING', + '1d' => 'ASN1_BMPSTRING', + ]; + + return array_key_exists($id, $asn1_Types) ? $asn1_Types[$id] : $id; } - } - - public static function __callStatic($func, $params) { - $func = strtolower($func); - $asn1Tag = self::asn1Tag($func); - if($asn1Tag !== false){ - $num = $asn1Tag; //valu of array - $hex = $params[0]; - $val = $hex; - if(in_array($func, ['printable', 'utf8', 'ia5', 'visible', 't61'])) { // ($string) - $val = bin2hex($hex); - } - if($func == 'int') { - $val = (strlen($val)%2 != 0)?"0$val":"$val"; + + /** + * parse asn.1 to array + * to be called from parse() function + * + * @param string $hex asn.1 hex form + * + * @return array asn.1 structure + */ + protected static function oneParse(string $hex): array|false + { + if ($hex === '') { + return false; } - if($func == 'expl') { //expl($num, $hex) - $num = $num.$params[0]; - $val = $params[1]; + + if (! @ctype_xdigit($hex) || @strlen($hex) % 2 !== 0) { + echo "input:\"{$hex}\" not hex string!.\n"; + + return false; } - if($func == 'impl') { //impl($num="0") - $val = (!$val)?"00":$val; - $val = (strlen($val)%2 != 0)?"0$val":$val; - return $num.$val; + + $stop = false; + while ($stop == false) { + $asn1_type = substr($hex, 0, 2); + $tlv_tagLength = hexdec(substr($hex, 2, 2)); + if ($tlv_tagLength > 127) { + $tlv_lengthLength = $tlv_tagLength - 128; + $tlv_valueLength = substr($hex, 4, $tlv_lengthLength * 2); + } else { + $tlv_lengthLength = 0; + $tlv_valueLength = substr($hex, 2, 2); + } + + if ($tlv_lengthLength > 4) { // limit tlv_lengthLength to FFFF + return false; + } + + $tlv_valueLength = hexdec($tlv_valueLength); + $totalTlLength = 2 + 2 + $tlv_lengthLength * 2; + $tlv_value = substr($hex, $totalTlLength, $tlv_valueLength * 2); + $remain = substr($hex, $totalTlLength + $tlv_valueLength * 2); + $newhexdump = substr($hex, 0, $totalTlLength + $tlv_valueLength * 2); + $result[] = [ + 'tlv_tagLength' => strlen(dechex($tlv_tagLength)) % 2 === 0 ? dechex($tlv_tagLength) : '0' . dechex($tlv_tagLength), + 'tlv_lengthLength' => $tlv_lengthLength, + 'tlv_valueLength' => $tlv_valueLength, + 'newhexdump' => $newhexdump, + 'typ' => $asn1_type, + 'tlv_value' => $tlv_value, + ]; + if ($remain === '') { // if remains string was empty & contents also empty, function return FALSE + $stop = true; + } else { + $hex = $remain; + } } - if($func == 'other') { //OTHER($id, $hex, $chr = false) - $id = $params[0]; - $hex = $params[1]; - $chr = @$params[2]; - $str = $hex; - if($chr != false) { - $str = bin2hex($hex); - } - $ret = "$id".self::asn1_header($str).$str; - return $ret; + + return $result; + } + + // =====End ASN.1 Parser section===== + + // =====Begin ASN.1 Builder section===== + /** + * create asn.1 TLV tag length, length length and value length + * to be called from asn.1 builder functions + * + * @param string $str string value of asn.1 + * + * @return string hex of asn.1 TLV tag length + */ + protected static function asn1_header(string $str): string + { + $len = strlen($str) / 2; + $ret = dechex($len); + if (strlen($ret) % 2 !== 0) { + $ret = '0' . $ret; } - if($func == 'utime') { - $time = $params[0]; //yymmddhhiiss - $oldTz = date_default_timezone_get(); - date_default_timezone_set("UTC"); - $time = date("ymdHis", $time); - date_default_timezone_set($oldTz); - $val = bin2hex($time."Z"); + + $headerLength = strlen($ret) / 2; + if ($len > 127) { + $ret = '8' . $headerLength . $ret; } - if($func == 'gtime') { - if(!$time = strtotime($params[0])) { - // echo "asn1::GTIME function strtotime cant recognize time!! please check at input=\"{$params[0]}\""; - return false; - } - $oldTz = date_default_timezone_get(); - // date_default_timezone_set("UTC"); - $time = date("YmdHis", $time); - date_default_timezone_set($oldTz); - $val = bin2hex($time."Z"); + + return $ret; + } + + /** + * create various dynamic function for asn1 + */ + private static function asn1Tag($name): string|false + { + $functionList = [ + 'seq' => '30', + 'oct' => '04', + 'obj' => '06', + 'bit' => '03', + 'printable' => '13', + 'int' => '02', + 'set' => '31', + 'expl' => 'a', + 'utime' => '17', + 'gtime' => '18', + 'utf8' => '0c', + 'ia5' => '16', + 'visible' => '1a', + 't61' => '14', + 'impl' => '80', + 'other' => '', + ]; + if (array_key_exists($name, $functionList)) { + return $functionList[$name]; } - $hdr = self::asn1_header($val); - return $num.$hdr.$val; - } else { - // echo "asn1 \"$func\" not exists!"; + + // echo "func \"$name\" not available"; + return false; + } - } - // =====End ASN.1 Builder section===== + + // =====End ASN.1 Builder section===== } diff --git a/src/helpers/contentgeneration.php b/src/helpers/contentgeneration.php index b87cf7a..f644833 100644 --- a/src/helpers/contentgeneration.php +++ b/src/helpers/contentgeneration.php @@ -21,33 +21,33 @@ namespace ddn\sapp\helpers; -use ddn\sapp\PDFBaseDoc; -use ddn\sapp\PDFBaseObject; -use ddn\sapp\pdfvalue\PDFValueObject; +use ddn\sapp\PDFException; use ddn\sapp\pdfvalue\PDFValueList; +use ddn\sapp\pdfvalue\PDFValueObject; use ddn\sapp\pdfvalue\PDFValueReference; use ddn\sapp\pdfvalue\PDFValueType; -use ddn\sapp\pdfvalue\PDFValueHexString; -use ddn\sapp\pdfvalue\PDFValueString; +use finfo; -use function ddn\sapp\helpers\get_random_string; -use function ddn\sapp\helpers\mime_to_ext; -use function ddn\sapp\helpers\_parsejpg; -use function ddn\sapp\helpers\_parsepng; -use function ddn\sapp\helpers\p_error; - -function tx($x, $y) { - return sprintf(" 1 0 0 1 %.2F %.2F cm", $x, $y); +function tx($x, $y): string +{ + return sprintf(' 1 0 0 1 %.2F %.2F cm', $x, $y); } -function sx($w, $h) { - return sprintf(" %.2F 0 0 %.2F 0 0 cm", $w, $h); + +function sx($w, $h): string +{ + return sprintf(' %.2F 0 0 %.2F 0 0 cm', $w, $h); } -function deg2rad($angle) { - return $angle * pi() / 180; + +function deg2rad($angle): float +{ + return $angle * M_PI / 180; } -function rx($angle) { + +function rx($angle): string +{ $angle = deg2rad($angle); - return sprintf(" %.2F %.2F %.2F %.2F 0 0 cm", cos($angle), sin($angle), -sin($angle), cos($angle)); + + return sprintf(' %.2F %.2F %.2F %.2F 0 0 cm', cos($angle), sin($angle), -sin($angle), cos($angle)); } /** @@ -55,24 +55,26 @@ function rx($angle) { * NOTE: the image inclusion is taken from http://www.fpdf.org/; this is a translation * of function _putimage */ -function _create_image_objects($info, $object_factory) { +function _create_image_objects($info, $object_factory): array +{ $objects = []; - $image = call_user_func($object_factory, + $image = call_user_func( + $object_factory, [ 'Type' => '/XObject', 'Subtype' => '/Image', 'Width' => $info['w'], 'Height' => $info['h'], - 'ColorSpace' => [ ], + 'ColorSpace' => [], 'BitsPerComponent' => $info['bpc'], - 'Length' => strlen($info['data']), - ] + 'Length' => strlen((string) $info['data']), + ] ); switch ($info['cs']) { case 'Indexed': - $data = gzcompress($info['pal']); + $data = gzcompress((string) $info['pal']); $streamobject = call_user_func($object_factory, [ 'Filter' => '/FlateDecode', 'Length' => strlen($data), @@ -80,41 +82,51 @@ function _create_image_objects($info, $object_factory) { $streamobject->set_stream($data); $image['ColorSpace']->push([ - '/Indexed', '/DeviceRGB', (strlen($info['pal']) / 3) - 1, new PDFValueReference($streamobject->get_oid()) + '/Indexed', + '/DeviceRGB', + strlen((string) $info['pal']) / 3 - 1, + new PDFValueReference($streamobject->get_oid()), ]); - array_push($objects, $streamobject); + $objects[] = $streamobject; break; case 'DeviceCMYK': - $image["Decode"] = new PDFValueList([1, 0, 1, 0, 1, 0, 1, 0]); + $image['Decode'] = new PDFValueList([1, 0, 1, 0, 1, 0, 1, 0]); + // no break default: - $image['ColorSpace'] = new PDFValueType( $info['cs'] ); + $image['ColorSpace'] = new PDFValueType($info['cs']); break; } - if (isset($info['f'])) + if (isset($info['f'])) { $image['Filter'] = new PDFValueType($info['f']); + } - if(isset($info['dp'])) + if (isset($info['dp'])) { $image['DecodeParms'] = PDFValueObject::fromstring($info['dp']); + } - if (isset($info['trns']) && is_array($info['trns'])) + if (isset($info['trns']) && is_array($info['trns'])) { $image['Mask'] = new PDFValueList($info['trns']); + } if (isset($info['smask'])) { $smaskinfo = [ - 'w' => $info['w'], - 'h' => $info['h'], - 'cs' => 'DeviceGray', - 'bpc' => 8, - 'f' => $info['f'], - 'dp' => '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns '.$info['w'], - 'data' => $info['smask'] + 'w' => $info['w'], + 'h' => $info['h'], + 'cs' => 'DeviceGray', + 'bpc' => 8, + 'f' => $info['f'], + 'dp' => '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' . $info['w'], + 'data' => $info['smask'], ]; // In principle, it may return multiple objects $smasks = _create_image_objects($smaskinfo, $object_factory); - foreach ($smasks as $smask) - array_push($objects, $smask); + assert($smasks !== []); + foreach ($smasks as $smask) { + $objects[] = $smask; + } + $image['SMask'] = new PDFValueReference($smask->get_oid()); } @@ -124,24 +136,28 @@ function _create_image_objects($info, $object_factory) { return $objects; } -function is_base64($string){ +function is_base64($string): bool +{ // Check if there are valid base64 characters - if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $string)) return false; + if (! preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', (string) $string)) { + return false; + } // Decode the string in strict mode and check the results - $decoded = base64_decode($string, true); - if(false === $decoded) return false; + $decoded = base64_decode((string) $string, true); + if ($decoded === false) { + return false; + } // Encode the string again - if(base64_encode($decoded) != $string) return false; - - return true; + return base64_encode($decoded) == $string; } /** * This function creates the objects needed to add an image to the document, at a specific position and size. * The function is agnostic from the place in which the image is to be created, and just creates the objects * with its contents and prepares the PDF command to place the image + * * @param filename the file name that contains the image, or a string that contains the image (with character '@' * prepended) * @param x points from left in which to appear the image (the units are "content-defined" (i.e. depending on the size of the page)) @@ -149,30 +165,33 @@ function is_base64($string){ * @param w width of the rectangle in which to appear the image (image will be scaled, and the units are "content-defined" (i.e. depending on the size of the page)) * @param h height of the rectangle in which to appear the image (image will be scaled, and the units are "content-defined" (i.e. depending on the size of the page)) * @param angle the rotation angle in degrees; the image will be rotated using the center - * @param keep_proportions if true, the image will keep the proportions when rotated, then the image will not occupy the full + * @param keep_proportions if true, the image will keep the proportions when rotated, then the image will not occupy the full + * * @return result an array with the next fields: * "images": objects of the corresponding images (i.e. position [0] is the image, the rest elements are masks, if needed) * "resources": PDFValueObject with keys that needs to be incorporated to the resources of the object in which the images will appear * "alpha": true if the image has alpha * "command": pdf command to draw the image */ -function _add_image($object_factory, $filename, $x=0, $y=0, $w=0, $h=0, $angle = 0, $keep_proportions = true) { - - if (empty($filename)) - return p_error('invalid image name or stream'); +function _add_image($object_factory, $filename, $x = 0, $y = 0, $w = 0, $h = 0, $angle = 0, bool $keep_proportions = true): array +{ + if (empty($filename)) { + throw new PDFException('invalid image name or stream'); + } if ($filename[0] === '@') { - $filecontent = substr($filename, 1); - } else if (is_base64($filename)) { - $filecontent = base64_decode($filename); + $filecontent = substr((string) $filename, 1); + } elseif (is_base64($filename)) { + $filecontent = base64_decode((string) $filename, true); } else { $filecontent = @file_get_contents($filename); - if ($filecontent === false) - return p_error("failed to get the image"); + if ($filecontent === false) { + throw new PDFException('failed to get the image'); + } } - $finfo = new \finfo(); + $finfo = new finfo(); $content_type = $finfo->buffer($filecontent, FILEINFO_MIME_TYPE); $ext = mime_to_ext($content_type); @@ -189,30 +208,39 @@ function _add_image($object_factory, $filename, $x=0, $y=0, $w=0, $h=0, $angle = $info = _parsepng($filecontent); break; default: - return p_error("unsupported mime type"); + throw new PDFException('unsupported mime type'); } // Generate a new identifier for the image - $info['i'] = "Im" . get_random_string(4); + $info['i'] = 'Im' . get_random_string(4); - if ($w === null) + if ($w === null) { $w = -96; - if ($h === null) + } + + if ($h === null) { $h = -96; + } - if($w<0) - $w = -$info['w']*72/$w; - if($h<0) - $h = -$info['h']*72/$h; - if($w==0) - $w = $h*$info['w']/$info['h']; - if($h==0) - $h = $w*$info['h']/$info['w']; + if ($w < 0) { + $w = -$info['w'] * 72 / $w; + } + + if ($h < 0) { + $h = -$info['h'] * 72 / $h; + } + + if ($w == 0) { + $w = $h * $info['w'] / $info['h']; + } + + if ($h == 0) { + $h = $w * $info['h'] / $info['w']; + } $images_objects = _create_image_objects($info, $object_factory); // Generate the command to translate and scale the image - $data = "q "; if ($keep_proportions) { $angleRads = deg2rad($angle); @@ -247,22 +275,28 @@ function _add_image($object_factory, $filename, $x=0, $y=0, $w=0, $h=0, $angle = $data .= sx($w, $h); */ - $data = "q"; + $data = 'q'; $data .= tx($x, $y); $data .= sx($w, $h); if ($angle != 0) { $data .= tx(0.5, 0.5); $data .= rx($angle); - $data .= tx(-0.5,-0.5); + $data .= tx(-0.5, -0.5); } - $data .= sprintf(" /%s Do Q", $info['i']); - $resources = new PDFValueObject( [ - 'ProcSet' => [ '/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI' ], - 'XObject' => new PDFValueObject ([ - $info['i'] => new PDFValueReference($images_objects[0]->get_oid()), - ]) + $data .= sprintf(' /%s Do Q', $info['i']); + + $resources = new PDFValueObject([ + 'ProcSet' => ['/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI'], + 'XObject' => new PDFValueObject([ + $info['i'] => new PDFValueReference($images_objects[0]->get_oid()), + ]), ]); - return [ "image" => $images_objects[0], 'command' => $data, 'resources' => $resources, 'alpha' => $add_alpha ]; + return [ + 'image' => $images_objects[0], + 'command' => $data, + 'resources' => $resources, + 'alpha' => $add_alpha, + ]; } diff --git a/src/helpers/fpdfhelpers.php b/src/helpers/fpdfhelpers.php index 561f0ab..12942b0 100644 --- a/src/helpers/fpdfhelpers.php +++ b/src/helpers/fpdfhelpers.php @@ -16,229 +16,268 @@ GNU General Public License for more details. You should have received a copy of the GNU Lesser General Public License - along with this program. If not, see . - - --------- + along with this program. If not, see . - The code in this file is an adaptation of a part of the code included in - fpdf version 1.82 as downloaded from (http://www.fpdf.org/es/dl.php?v=182&f=tgz) + --------- - The fpdf license: + The code in this file is an adaptation of a part of the code included in + fpdf version 1.82 as downloaded from (http://www.fpdf.org/es/dl.php?v=182&f=tgz) - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software to use, copy, modify, distribute, sublicense, and/or sell - copies of the software, and to permit persons to whom the software is furnished - to do so. + The fpdf license: - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software to use, copy, modify, distribute, sublicense, and/or sell + copies of the software, and to permit persons to whom the software is furnished + to do so. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. */ namespace ddn\sapp\helpers; -use function ddn\sapp\helpers\p_error; -function _parsejpg($filecontent) +use ddn\sapp\PDFException; + +function _parsejpg($filecontent): array { - // Extract info from a JPEG file - $a = getimagesizefromstring($filecontent); - if(!$a) - return p_error('Missing or incorrect image'); - if($a[2]!=2) - return perror('Not a JPEG image'); - if(!isset($a['channels']) || $a['channels']==3) - $colspace = 'DeviceRGB'; - elseif($a['channels']==4) - $colspace = 'DeviceCMYK'; - else - $colspace = 'DeviceGray'; - $bpc = isset($a['bits']) ? $a['bits'] : 8; - $data = $filecontent; - return array('w'=>$a[0], 'h'=>$a[1], 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'DCTDecode', 'data'=>$data); + // Extract info from a JPEG file + $a = getimagesizefromstring($filecontent); + if (! $a) { + throw new PDFException('Missing or incorrect image'); + } + + if ($a[2] != 2) { + throw new PDFException('Not a JPEG image'); + } + + if (! isset($a['channels']) || $a['channels'] == 3) { + $colspace = 'DeviceRGB'; + } elseif ($a['channels'] == 4) { + $colspace = 'DeviceCMYK'; + } else { + $colspace = 'DeviceGray'; + } + + $bpc = $a['bits'] ?? 8; + $data = $filecontent; + + return [ + 'w' => $a[0], + 'h' => $a[1], + 'cs' => $colspace, + 'bpc' => $bpc, + 'f' => 'DCTDecode', + 'data' => $data, + ]; } -function _parsepng($filecontent) +function _parsepng($filecontent): array { - // Extract info from a PNG file - $f = new StreamReader($filecontent); - $info = _parsepngstream($f); - return $info; + // Extract info from a PNG file + $f = new StreamReader($filecontent); + + return _parsepngstream($f); } -function _parsepngstream(&$f) +function _parsepngstream(&$f): array { - // Check signature - if(($res=_readstream($f,8))!=chr(137).'PNG'.chr(13).chr(10).chr(26).chr(10)) - return p_error("Not a PNG image $res"); - - // Read header chunk - _readstream($f,4); - if(_readstream($f,4)!='IHDR') - return p_error('Incorrect PNG image'); - $w = _readint($f); - $h = _readint($f); - $bpc = ord(_readstream($f,1)); - if($bpc>8) - return p_error('16-bit depth not supported'); - $ct = ord(_readstream($f,1)); - if($ct==0 || $ct==4) - $colspace = 'DeviceGray'; - elseif($ct==2 || $ct==6) - $colspace = 'DeviceRGB'; - elseif($ct==3) - $colspace = 'Indexed'; - else - return p_error('Unknown color type'); - if(ord(_readstream($f,1))!=0) - return p_error('Unknown compression method'); - if(ord(_readstream($f,1))!=0) - return p_error('Unknown filter method'); - if(ord(_readstream($f,1))!=0) - return p_error('Interlacing not supported'); - _readstream($f,4); - $dp = '/Predictor 15 /Colors '.($colspace=='DeviceRGB' ? 3 : 1).' /BitsPerComponent '.$bpc.' /Columns '.$w; - - // Scan chunks looking for palette, transparency and image data - $pal = ''; - $trns = ''; - $data = ''; - do - { - $n = _readint($f); - $type = _readstream($f,4); - if($type=='PLTE') - { - // Read palette - $pal = _readstream($f,$n); - _readstream($f,4); - } - elseif($type=='tRNS') - { - // Read transparency info - $t = _readstream($f,$n); - if($ct==0) - $trns = array(ord(substr($t,1,1))); - elseif($ct==2) - $trns = array(ord(substr($t,1,1)), ord(substr($t,3,1)), ord(substr($t,5,1))); - else - { - $pos = strpos($t,chr(0)); - if($pos!==false) - $trns = array($pos); - } - _readstream($f,4); - } - elseif($type=='IDAT') - { - // Read image data block - $data .= _readstream($f,$n); - _readstream($f,4); - } - elseif($type=='IEND') - break; - else - _readstream($f,$n+4); - } - while($n); - - if($colspace=='Indexed' && empty($pal)) - return p_error('Missing palette in image'); - $info = array('w'=>$w, 'h'=>$h, 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'FlateDecode', 'dp'=>$dp, 'pal'=>$pal, 'trns'=>$trns); - if($ct>=4) - { - // Extract alpha channel - if(!function_exists('gzuncompress')) - return p_error('Zlib not available, can\'t handle alpha channel'); - $data = gzuncompress($data); - if ($data === false) - return p_error('failed to uncompress the image'); - $color = ''; - $alpha = ''; - if($ct==4) - { - // Gray image - $len = 2*$w; - for($i=0;$i<$h;$i++) - { - $pos = (1+$len)*$i; - $color .= $data[$pos]; - $alpha .= $data[$pos]; - $line = substr($data,$pos+1,$len); - $color .= preg_replace('/(.)./s','$1',$line); - $alpha .= preg_replace('/.(.)/s','$1',$line); - } - } - else - { - // RGB image - $len = 4*$w; - for($i=0;$i<$h;$i++) - { - $pos = (1+$len)*$i; - $color .= $data[$pos]; - $alpha .= $data[$pos]; - $line = substr($data,$pos+1,$len); - $color .= preg_replace('/(.{3})./s','$1',$line); - $alpha .= preg_replace('/.{3}(.)/s','$1',$line); - } - } - unset($data); - $data = gzcompress($color); - $info['smask'] = gzcompress($alpha); - /* - $this->WithAlpha = true; - if($this->PDFVersion<'1.4') - $this->PDFVersion = '1.4'; - */ - } - $info['data'] = $data; - return $info; -} + // Check signature + if (($res = _readstream($f, 8)) !== chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) { + throw new PDFException('Not a PNG image ' . $res); + } + + // Read header chunk + _readstream($f, 4); + if (_readstream($f, 4) !== 'IHDR') { + throw new PDFException('Incorrect PNG image'); + } + + $w = _readint($f); + $h = _readint($f); + $bpc = ord(_readstream($f, 1)); + if ($bpc > 8) { + throw new PDFException('16-bit depth not supported'); + } + + $ct = ord(_readstream($f, 1)); + if ($ct == 0 || $ct == 4) { + $colspace = 'DeviceGray'; + } elseif ($ct == 2 || $ct == 6) { + $colspace = 'DeviceRGB'; + } elseif ($ct == 3) { + $colspace = 'Indexed'; + } else { + throw new PDFException('Unknown color type'); + } + + if (ord(_readstream($f, 1)) != 0) { + throw new PDFException('Unknown compression method'); + } + + if (ord(_readstream($f, 1)) !== 0) { + throw new PDFException('Unknown filter method'); + } + + if (ord(_readstream($f, 1)) !== 0) { + throw new PDFException('Interlacing not supported'); + } + + _readstream($f, 4); + $dp = '/Predictor 15 /Colors ' . ($colspace === 'DeviceRGB' ? 3 : 1) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w; -function _readstream(&$f, $n) { - $res = ""; + // Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $data = ''; + do { + $n = _readint($f); + $type = _readstream($f, 4); + if ($type === 'PLTE') { + // Read palette + $pal = _readstream($f, $n); + _readstream($f, 4); + } elseif ($type === 'tRNS') { + // Read transparency info + $t = _readstream($f, $n); + if ($ct == 0) { + $trns = [ord($t[1])]; + } elseif ($ct == 2) { + $trns = [ord($t[1]), ord($t[3]), ord($t[5])]; + } else { + $pos = strpos($t, chr(0)); + if ($pos !== false) { + $trns = [$pos]; + } + } - while ($n>0 && !$f->eos()) { - $s = $f->nextchars($n); - if ($s === false) - return p_error("Error while reading the stream"); - $n -= strlen($s); - $res .= $s; - } + _readstream($f, 4); + } elseif ($type === 'IDAT') { + // Read image data block + $data .= _readstream($f, $n); + _readstream($f, 4); + } elseif ($type === 'IEND') { + break; + } else { + _readstream($f, $n + 4); + } + } while ($n); - if ($n>0) - return p_error('Unexpected end of stream'); - return $res; + if ($colspace === 'Indexed' && ($pal === '' || $pal === '0')) { + throw new PDFException('Missing palette in image'); + } + + $info = [ + 'w' => $w, + 'h' => $h, + 'cs' => $colspace, + 'bpc' => $bpc, + 'f' => 'FlateDecode', + 'dp' => $dp, + 'pal' => $pal, + 'trns' => $trns, + ]; + if ($ct >= 4) { + // Extract alpha channel + if (! function_exists('gzuncompress')) { + throw new PDFException("Zlib not available, can't handle alpha channel"); + } + + $data = gzuncompress($data); + if ($data === false) { + throw new PDFException('failed to uncompress the image'); + } + + $color = ''; + $alpha = ''; + if ($ct == 4) { + // Gray image + $len = 2 * $w; + for ($i = 0; $i < $h; $i++) { + $pos = (1 + $len) * $i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data, $pos + 1, $len); + $color .= preg_replace('/(.)./s', '$1', $line); + $alpha .= preg_replace('/.(.)/s', '$1', $line); + } + } else { + // RGB image + $len = 4 * $w; + for ($i = 0; $i < $h; $i++) { + $pos = (1 + $len) * $i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data, $pos + 1, $len); + $color .= preg_replace('/(.{3})./s', '$1', $line); + $alpha .= preg_replace('/.{3}(.)/s', '$1', $line); + } + } + + unset($data); + $data = gzcompress($color); + $info['smask'] = gzcompress($alpha); + /* + $this->WithAlpha = true; + if($this->PDFVersion<'1.4') + $this->PDFVersion = '1.4'; + */ + } + + $info['data'] = $data; + + return $info; } -function _readint(&$f) +function _readstream($f, $n): string { - // Read a 4-byte integer from stream - $a = unpack('Ni',_readstream($f,4)); - return $a['i']; + $res = ''; + + while ($n > 0 && ! $f->eos()) { + $s = $f->nextchars($n); + if ($s === false) { + throw new PDFException('Error while reading the stream'); + } + + $n -= strlen((string) $s); + $res .= $s; + } + + if ($n > 0) { + throw new PDFException('Unexpected end of stream'); + } + + return $res; } +function _readint($f) +{ + // Read a 4-byte integer from stream + $a = unpack('Ni', _readstream($f, 4)); + + return $a['i']; +} /* function _readstream($f, $n) { - // Read n bytes from stream - $res = ''; - while($n>0 && !feof($f)) - { - $s = fread($f,$n); - if($s===false) - return p_error('Error while reading stream'); - $n -= strlen($s); - $res .= $s; - } - if($n>0) - return p_error('Unexpected end of stream'); - return $res; + // Read n bytes from stream + $res = ''; + while($n>0 && !feof($f)) + { + $s = fread($f,$n); + if($s===false) + throw new PDFException('Error while reading stream'); + $n -= strlen($s); + $res .= $s; + } + if($n>0) + throw new PDFException('Unexpected end of stream'); + return $res; } function _readint($f) { - // Read a 4-byte integer from stream - $a = unpack('Ni',_readstream($f,4)); - return $a['i']; + // Read a 4-byte integer from stream + $a = unpack('Ni',_readstream($f,4)); + return $a['i']; } -*/ \ No newline at end of file +*/ diff --git a/src/helpers/helpers.php b/src/helpers/helpers.php index edcd305..0c2e88b 100644 --- a/src/helpers/helpers.php +++ b/src/helpers/helpers.php @@ -21,7 +21,10 @@ namespace ddn\sapp\helpers; -if (! defined("_DEBUG_LEVEL")) { +use DateTime; +use DateTimeInterface; + +if (! defined('_DEBUG_LEVEL')) { define('_DEBUG_LEVEL', 3); } @@ -29,156 +32,117 @@ define('STDERR', fopen('php://stderr', 'wb')); } -/** +/** * Outputs a var to a string, using the PHP var_dump function + * * @param var the variable to output + * * @return output the result of the var_dump of the variable -*/ -function var_dump_to_string($var) { + */ +function var_dump_to_string($var): string|false +{ ob_start(); var_dump($var); - $result = ob_get_clean(); - return $result; + + return ob_get_clean(); } + /** * Outputs a set of vars to a string, that is returned + * * @param vars the vars to dump + * * @return str the var_dump output of the variables */ -function debug_var(...$vars) { +function debug_var(...$vars): ?string +{ // If the debug level is less than 3, suppress debug messages - if (_DEBUG_LEVEL < 3) return; + if (_DEBUG_LEVEL < 3) { + return null; + } $result = []; foreach ($vars as $var) { - array_push($result, var_dump_to_string($var)); + $result[] = var_dump_to_string($var); } + return implode("\n", $result); } -/** - * Function that writes the representation of some vars to - * @param vars comma separated list of variables to output - */ -function p_debug_var(...$vars) { - // If the debug level is less than 3, suppress debug messages - if (_DEBUG_LEVEL < 3) return; - - foreach ($vars as $var) { - $e = var_dump_to_string($var); - p_stderr($e, "Debug"); - } -} + /** * Function that converts an array into a string, but also recursively converts its values * just in case that they are also arrays. In case that it is not an array, it returns its * string representation + * * @param e the variable to convert + * * @return str the string representation of the array */ -function varval($e) { +function varval($e) +{ $retval = $e; if (is_array($e)) { $a = []; foreach ($e as $k => $v) { $v = varval($v); - array_push($a, "$k => $v"); + $a[] = sprintf('%s => %s', $k, $v); } - $retval = "[ " . implode(", ", $a) . " ]"; - } - return $retval; -} -/** - * Function that writes a string to stderr, including some information about the call stack - * @param e the string to write to stderr - * @param tag the tag to prepend to the string and the debug information - * @param level the depth level to output (0 will refer to the function that called p_stderr - * call itself, 1 to the function that called to the function that called p_stderr) - */ -function p_stderr(&$e, $tag = "Error", $level = 1) { - $dinfo = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); - $dinfo = $dinfo[$level]; - $e = sprintf("$tag info at %s:%d: %s", $dinfo['file'], $dinfo['line'], varval($e)); - fwrite(STDERR, "$e\n"); -} -/** - * Function that writes a string to stderr and returns a value (to ease coding like return p_debug(...)) - * @param e the debug message - * @param retval the value to return (default: false) - * @return retval - */ -function p_debug($e, $retval = false) { - // If the debug level is less than 3, suppress debug messages - if (_DEBUG_LEVEL >= 3) { - p_stderr($e, "Debug"); - } - return $retval; -} -/** - * Function that writes a string to stderr and returns a value (to ease coding like return p_warning(...)) - * @param e the debug message - * @param retval the value to return (default: false) - * @return retval - */ -function p_warning($e, $retval = false) { - // If the debug level is less than 2, suppress warning messages - if (_DEBUG_LEVEL >= 2) { - p_stderr($e, "Warning"); - } - return $retval; -} -/** - * Function that writes a string to stderr and returns a value (to ease coding like return p_error(...)) - * @param e the error message - * @param retval the value to return (default: false) - * @return retval - */ -function p_error($e, $retval = false) { - // If the debug level is less than 1, suppress error messages - if (_DEBUG_LEVEL >= 1) { - p_stderr($e, "Error"); + + $retval = '[ ' . implode(', ', $a) . ' ]'; } + return $retval; } + /** * Obtains a random string from a printable character set: alphanumeric, extended with * common symbols, an extended with less common symbols. * Note: does not consider space (0x20) nor delete (0x7f) for the alphabet. All the * other printable ascii chars are considered + * * @param length length of the resulting random string (default: 8) * @param extended true if the alphabet should consider also the common symbols (e.g. :,(...)) - * @param hard true if the alphabet should consider also the hard symbols: ^`|~ (which use to + * @param hard true if the alphabet should consider also the hard symbols: ^`|~ (which use to * need more than one key to be written) + * * @return random_string a random string considering the alphabet */ -function get_random_string($length = 8, $extended = false, $hard = false){ - $token = ""; - $codeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - $codeAlphabet.= "abcdefghijklmnopqrstuvwxyz"; - $codeAlphabet.= "0123456789"; +function get_random_string($length = 8, $extended = false, $hard = false): string +{ + $token = ''; + $codeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $codeAlphabet .= 'abcdefghijklmnopqrstuvwxyz'; + $codeAlphabet .= '0123456789'; if ($extended === true) { $codeAlphabet .= "!\"#$%&'()*+,-./:;<=>?@[\\]_{}"; } + if ($hard === true) { - $codeAlphabet .= "^`|~"; + $codeAlphabet .= '^`|~'; } + $max = strlen($codeAlphabet); - for ($i=0; $i < $length; $i++) { - $token .= $codeAlphabet[random_int(0, $max-1)]; + for ($i = 0; $i < $length; $i++) { + $token .= $codeAlphabet[random_int(0, $max - 1)]; } - return $token; -} -function get_memory_limit() { + return $token; +} + +function get_memory_limit(): int +{ $memory_limit = ini_get('memory_limit'); if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches) === 1) { - $memory_limit = intval($matches[1]); + $memory_limit = (int) $matches[1]; switch ($matches[2]) { case 'G': - $memory_limit = $memory_limit * 1024; + $memory_limit *= 1024; + // no break case 'M': - $memory_limit = $memory_limit * 1024; + $memory_limit *= 1024; + // no break case 'K': - $memory_limit = $memory_limit * 1024; + $memory_limit *= 1024; break; default: $memory_limit = 0; @@ -186,44 +150,57 @@ function get_memory_limit() { } else { $memory_limit = 0; } + return $memory_limit; } -function show_bytes($str, $columns = null) { - $result = ""; - if ($columns === null) - $columns = strlen($str); +function show_bytes($str, $columns = null): string +{ + $result = ''; + if ($columns === null) { + $columns = strlen((string) $str); + } + $c = $columns; - for ($i = 0; $i < strlen($str); $i++) { - $result .= sprintf("%02x ", ord($str[$i])); + for ($i = 0, $iMax = strlen((string) $str); $i < $iMax; $i++) { + $result .= sprintf('%02x ', ord($str[$i])); $c--; if ($c === 0) { $c = $columns; $result .= "\n"; } - } + return $result; } /** * Function that outputs a timestamp to a PDF compliant string (including the D:) + * * @param timestamp the timestamp to conver (or 0 if get "now") + * * @return date_string the date string in PDF format */ -function timestamp_to_pdfdatestring($date = null) { - if ($date === null) - $date = new \DateTime(); +function timestamp_to_pdfdatestring(?DateTimeInterface $date = null): string +{ + if (! $date instanceof DateTimeInterface) { + $date = new DateTime(); + } $timestamp = $date->getTimestamp(); + return 'D:' . get_pdf_formatted_date($timestamp); } + /** * Returns a formatted date-time. + * * @param $time (int) Time in seconds. + * * @return string escaped date string. * @since 5.9.152 (2012-03-23) */ -function get_pdf_formatted_date($time) { - return substr_replace(date('YmdHisO', intval($time)), '\'', (0 - 2), 0).'\''; +function get_pdf_formatted_date(int $time): string +{ + return substr_replace(date('YmdHisO', $time), "'", -2, 0) . "'"; } diff --git a/src/helpers/mime.php b/src/helpers/mime.php index 29145fb..1ec1677 100644 --- a/src/helpers/mime.php +++ b/src/helpers/mime.php @@ -21,187 +21,189 @@ namespace ddn\sapp\helpers; -function mime_to_ext($mime) { +function mime_to_ext($mime): string|false +{ $mime_map = [ - 'video/3gpp2' => '3g2', - 'video/3gp' => '3gp', - 'video/3gpp' => '3gp', - 'application/x-compressed' => '7zip', - 'audio/x-acc' => 'aac', - 'audio/ac3' => 'ac3', - 'application/postscript' => 'ai', - 'audio/x-aiff' => 'aif', - 'audio/aiff' => 'aif', - 'audio/x-au' => 'au', - 'video/x-msvideo' => 'avi', - 'video/msvideo' => 'avi', - 'video/avi' => 'avi', - 'application/x-troff-msvideo' => 'avi', - 'application/macbinary' => 'bin', - 'application/mac-binary' => 'bin', - 'application/x-binary' => 'bin', - 'application/x-macbinary' => 'bin', - 'image/bmp' => 'bmp', - 'image/x-bmp' => 'bmp', - 'image/x-bitmap' => 'bmp', - 'image/x-xbitmap' => 'bmp', - 'image/x-win-bitmap' => 'bmp', - 'image/x-windows-bmp' => 'bmp', - 'image/ms-bmp' => 'bmp', - 'image/x-ms-bmp' => 'bmp', - 'application/bmp' => 'bmp', - 'application/x-bmp' => 'bmp', - 'application/x-win-bitmap' => 'bmp', - 'application/cdr' => 'cdr', - 'application/coreldraw' => 'cdr', - 'application/x-cdr' => 'cdr', - 'application/x-coreldraw' => 'cdr', - 'image/cdr' => 'cdr', - 'image/x-cdr' => 'cdr', - 'zz-application/zz-winassoc-cdr' => 'cdr', - 'application/mac-compactpro' => 'cpt', - 'application/pkix-crl' => 'crl', - 'application/pkcs-crl' => 'crl', - 'application/x-x509-ca-cert' => 'crt', - 'application/pkix-cert' => 'crt', - 'text/css' => 'css', - 'text/x-comma-separated-values' => 'csv', - 'text/comma-separated-values' => 'csv', - 'application/vnd.msexcel' => 'csv', - 'application/x-director' => 'dcr', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', - 'application/x-dvi' => 'dvi', - 'message/rfc822' => 'eml', - 'application/x-msdownload' => 'exe', - 'video/x-f4v' => 'f4v', - 'audio/x-flac' => 'flac', - 'video/x-flv' => 'flv', - 'image/gif' => 'gif', - 'application/gpg-keys' => 'gpg', - 'application/x-gtar' => 'gtar', - 'application/x-gzip' => 'gzip', - 'application/mac-binhex40' => 'hqx', - 'application/mac-binhex' => 'hqx', - 'application/x-binhex40' => 'hqx', - 'application/x-mac-binhex40' => 'hqx', - 'text/html' => 'html', - 'image/x-icon' => 'ico', - 'image/x-ico' => 'ico', - 'image/vnd.microsoft.icon' => 'ico', - 'text/calendar' => 'ics', - 'application/java-archive' => 'jar', - 'application/x-java-application' => 'jar', - 'application/x-jar' => 'jar', - 'image/jp2' => 'jp2', - 'video/mj2' => 'jp2', - 'image/jpx' => 'jp2', - 'image/jpm' => 'jp2', - 'image/jpeg' => 'jpg', - 'image/pjpeg' => 'jpg', - 'application/x-javascript' => 'js', - 'application/json' => 'json', - 'text/json' => 'json', - 'application/vnd.google-earth.kml+xml' => 'kml', - 'application/vnd.google-earth.kmz' => 'kmz', - 'text/x-log' => 'log', - 'audio/x-m4a' => 'm4a', - 'audio/mp4' => 'm4a', - 'application/vnd.mpegurl' => 'm4u', - 'audio/midi' => 'mid', - 'application/vnd.mif' => 'mif', - 'video/quicktime' => 'mov', - 'video/x-sgi-movie' => 'movie', - 'audio/mpeg' => 'mp3', - 'audio/mpg' => 'mp3', - 'audio/mpeg3' => 'mp3', - 'audio/mp3' => 'mp3', - 'video/mp4' => 'mp4', - 'video/mpeg' => 'mpeg', - 'application/oda' => 'oda', - 'audio/ogg' => 'ogg', - 'video/ogg' => 'ogg', - 'application/ogg' => 'ogg', - 'application/x-pkcs10' => 'p10', - 'application/pkcs10' => 'p10', - 'application/x-pkcs12' => 'p12', - 'application/x-pkcs7-signature' => 'p7a', - 'application/pkcs7-mime' => 'p7c', - 'application/x-pkcs7-mime' => 'p7c', - 'application/x-pkcs7-certreqresp' => 'p7r', - 'application/pkcs7-signature' => 'p7s', - 'application/pdf' => 'pdf', - 'application/octet-stream' => 'pdf', - 'application/x-x509-user-cert' => 'pem', - 'application/x-pem-file' => 'pem', - 'application/pgp' => 'pgp', - 'application/x-httpd-php' => 'php', - 'application/php' => 'php', - 'application/x-php' => 'php', - 'text/php' => 'php', - 'text/x-php' => 'php', - 'application/x-httpd-php-source' => 'php', - 'image/png' => 'png', - 'image/x-png' => 'png', - 'application/powerpoint' => 'ppt', - 'application/vnd.ms-powerpoint' => 'ppt', - 'application/vnd.ms-office' => 'ppt', - 'application/msword' => 'ppt', + 'video/3gpp2' => '3g2', + 'video/3gp' => '3gp', + 'video/3gpp' => '3gp', + 'application/x-compressed' => '7zip', + 'audio/x-acc' => 'aac', + 'audio/ac3' => 'ac3', + 'application/postscript' => 'ai', + 'audio/x-aiff' => 'aif', + 'audio/aiff' => 'aif', + 'audio/x-au' => 'au', + 'video/x-msvideo' => 'avi', + 'video/msvideo' => 'avi', + 'video/avi' => 'avi', + 'application/x-troff-msvideo' => 'avi', + 'application/macbinary' => 'bin', + 'application/mac-binary' => 'bin', + 'application/x-binary' => 'bin', + 'application/x-macbinary' => 'bin', + 'image/bmp' => 'bmp', + 'image/x-bmp' => 'bmp', + 'image/x-bitmap' => 'bmp', + 'image/x-xbitmap' => 'bmp', + 'image/x-win-bitmap' => 'bmp', + 'image/x-windows-bmp' => 'bmp', + 'image/ms-bmp' => 'bmp', + 'image/x-ms-bmp' => 'bmp', + 'application/bmp' => 'bmp', + 'application/x-bmp' => 'bmp', + 'application/x-win-bitmap' => 'bmp', + 'application/cdr' => 'cdr', + 'application/coreldraw' => 'cdr', + 'application/x-cdr' => 'cdr', + 'application/x-coreldraw' => 'cdr', + 'image/cdr' => 'cdr', + 'image/x-cdr' => 'cdr', + 'zz-application/zz-winassoc-cdr' => 'cdr', + 'application/mac-compactpro' => 'cpt', + 'application/pkix-crl' => 'crl', + 'application/pkcs-crl' => 'crl', + 'application/x-x509-ca-cert' => 'crt', + 'application/pkix-cert' => 'crt', + 'text/css' => 'css', + 'text/x-comma-separated-values' => 'csv', + 'text/comma-separated-values' => 'csv', + 'application/vnd.msexcel' => 'csv', + 'application/x-director' => 'dcr', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/x-dvi' => 'dvi', + 'message/rfc822' => 'eml', + 'application/x-msdownload' => 'exe', + 'video/x-f4v' => 'f4v', + 'audio/x-flac' => 'flac', + 'video/x-flv' => 'flv', + 'image/gif' => 'gif', + 'application/gpg-keys' => 'gpg', + 'application/x-gtar' => 'gtar', + 'application/x-gzip' => 'gzip', + 'application/mac-binhex40' => 'hqx', + 'application/mac-binhex' => 'hqx', + 'application/x-binhex40' => 'hqx', + 'application/x-mac-binhex40' => 'hqx', + 'text/html' => 'html', + 'image/x-icon' => 'ico', + 'image/x-ico' => 'ico', + 'image/vnd.microsoft.icon' => 'ico', + 'text/calendar' => 'ics', + 'application/java-archive' => 'jar', + 'application/x-java-application' => 'jar', + 'application/x-jar' => 'jar', + 'image/jp2' => 'jp2', + 'video/mj2' => 'jp2', + 'image/jpx' => 'jp2', + 'image/jpm' => 'jp2', + 'image/jpeg' => 'jpg', + 'image/pjpeg' => 'jpg', + 'application/x-javascript' => 'js', + 'application/json' => 'json', + 'text/json' => 'json', + 'application/vnd.google-earth.kml+xml' => 'kml', + 'application/vnd.google-earth.kmz' => 'kmz', + 'text/x-log' => 'log', + 'audio/x-m4a' => 'm4a', + 'audio/mp4' => 'm4a', + 'application/vnd.mpegurl' => 'm4u', + 'audio/midi' => 'mid', + 'application/vnd.mif' => 'mif', + 'video/quicktime' => 'mov', + 'video/x-sgi-movie' => 'movie', + 'audio/mpeg' => 'mp3', + 'audio/mpg' => 'mp3', + 'audio/mpeg3' => 'mp3', + 'audio/mp3' => 'mp3', + 'video/mp4' => 'mp4', + 'video/mpeg' => 'mpeg', + 'application/oda' => 'oda', + 'audio/ogg' => 'ogg', + 'video/ogg' => 'ogg', + 'application/ogg' => 'ogg', + 'application/x-pkcs10' => 'p10', + 'application/pkcs10' => 'p10', + 'application/x-pkcs12' => 'p12', + 'application/x-pkcs7-signature' => 'p7a', + 'application/pkcs7-mime' => 'p7c', + 'application/x-pkcs7-mime' => 'p7c', + 'application/x-pkcs7-certreqresp' => 'p7r', + 'application/pkcs7-signature' => 'p7s', + 'application/pdf' => 'pdf', + 'application/octet-stream' => 'pdf', + 'application/x-x509-user-cert' => 'pem', + 'application/x-pem-file' => 'pem', + 'application/pgp' => 'pgp', + 'application/x-httpd-php' => 'php', + 'application/php' => 'php', + 'application/x-php' => 'php', + 'text/php' => 'php', + 'text/x-php' => 'php', + 'application/x-httpd-php-source' => 'php', + 'image/png' => 'png', + 'image/x-png' => 'png', + 'application/powerpoint' => 'ppt', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/vnd.ms-office' => 'ppt', + 'application/msword' => 'ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', - 'application/x-photoshop' => 'psd', - 'image/vnd.adobe.photoshop' => 'psd', - 'audio/x-realaudio' => 'ra', - 'audio/x-pn-realaudio' => 'ram', - 'application/x-rar' => 'rar', - 'application/rar' => 'rar', - 'application/x-rar-compressed' => 'rar', - 'audio/x-pn-realaudio-plugin' => 'rpm', - 'application/x-pkcs7' => 'rsa', - 'text/rtf' => 'rtf', - 'text/richtext' => 'rtx', - 'video/vnd.rn-realvideo' => 'rv', - 'application/x-stuffit' => 'sit', - 'application/smil' => 'smil', - 'text/srt' => 'srt', - 'image/svg+xml' => 'svg', - 'application/x-shockwave-flash' => 'swf', - 'application/x-tar' => 'tar', - 'application/x-gzip-compressed' => 'tgz', - 'image/tiff' => 'tiff', - 'text/plain' => 'txt', - 'text/x-vcard' => 'vcf', - 'application/videolan' => 'vlc', - 'text/vtt' => 'vtt', - 'audio/x-wav' => 'wav', - 'audio/wave' => 'wav', - 'audio/wav' => 'wav', - 'application/wbxml' => 'wbxml', - 'video/webm' => 'webm', - 'audio/x-ms-wma' => 'wma', - 'application/wmlc' => 'wmlc', - 'video/x-ms-wmv' => 'wmv', - 'video/x-ms-asf' => 'wmv', - 'application/xhtml+xml' => 'xhtml', - 'application/excel' => 'xl', - 'application/msexcel' => 'xls', - 'application/x-msexcel' => 'xls', - 'application/x-ms-excel' => 'xls', - 'application/x-excel' => 'xls', - 'application/x-dos_ms_excel' => 'xls', - 'application/xls' => 'xls', - 'application/x-xls' => 'xls', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', - 'application/vnd.ms-excel' => 'xlsx', - 'application/xml' => 'xml', - 'text/xml' => 'xml', - 'text/xsl' => 'xsl', - 'application/xspf+xml' => 'xspf', - 'application/x-compress' => 'z', - 'application/x-zip' => 'zip', - 'application/zip' => 'zip', - 'application/x-zip-compressed' => 'zip', - 'application/s-compressed' => 'zip', - 'multipart/x-zip' => 'zip', - 'text/x-scriptzsh' => 'zsh', + 'application/x-photoshop' => 'psd', + 'image/vnd.adobe.photoshop' => 'psd', + 'audio/x-realaudio' => 'ra', + 'audio/x-pn-realaudio' => 'ram', + 'application/x-rar' => 'rar', + 'application/rar' => 'rar', + 'application/x-rar-compressed' => 'rar', + 'audio/x-pn-realaudio-plugin' => 'rpm', + 'application/x-pkcs7' => 'rsa', + 'text/rtf' => 'rtf', + 'text/richtext' => 'rtx', + 'video/vnd.rn-realvideo' => 'rv', + 'application/x-stuffit' => 'sit', + 'application/smil' => 'smil', + 'text/srt' => 'srt', + 'image/svg+xml' => 'svg', + 'application/x-shockwave-flash' => 'swf', + 'application/x-tar' => 'tar', + 'application/x-gzip-compressed' => 'tgz', + 'image/tiff' => 'tiff', + 'text/plain' => 'txt', + 'text/x-vcard' => 'vcf', + 'application/videolan' => 'vlc', + 'text/vtt' => 'vtt', + 'audio/x-wav' => 'wav', + 'audio/wave' => 'wav', + 'audio/wav' => 'wav', + 'application/wbxml' => 'wbxml', + 'video/webm' => 'webm', + 'audio/x-ms-wma' => 'wma', + 'application/wmlc' => 'wmlc', + 'video/x-ms-wmv' => 'wmv', + 'video/x-ms-asf' => 'wmv', + 'application/xhtml+xml' => 'xhtml', + 'application/excel' => 'xl', + 'application/msexcel' => 'xls', + 'application/x-msexcel' => 'xls', + 'application/x-ms-excel' => 'xls', + 'application/x-excel' => 'xls', + 'application/x-dos_ms_excel' => 'xls', + 'application/xls' => 'xls', + 'application/x-xls' => 'xls', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.ms-excel' => 'xlsx', + 'application/xml' => 'xml', + 'text/xml' => 'xml', + 'text/xsl' => 'xsl', + 'application/xspf+xml' => 'xspf', + 'application/x-compress' => 'z', + 'application/x-zip' => 'zip', + 'application/zip' => 'zip', + 'application/x-zip-compressed' => 'zip', + 'application/s-compressed' => 'zip', + 'multipart/x-zip' => 'zip', + 'text/x-scriptzsh' => 'zsh', ]; - return isset($mime_map[$mime]) ? $mime_map[$mime] : false; -} + + return $mime_map[$mime] ?? false; +} diff --git a/src/helpers/x509.php b/src/helpers/x509.php index af4a000..0b33563 100644 --- a/src/helpers/x509.php +++ b/src/helpers/x509.php @@ -1,5 +1,7 @@ '06082A864886F70D0202', - 'md4'=>'06082A864886F70D0204', - 'md5'=>'06082A864886F70D0205', - 'sha1'=>'06052B0E03021A', - 'sha224'=>'0609608648016503040204', - 'sha256'=>'0609608648016503040201', - 'sha384'=>'0609608648016503040202', - 'sha512'=>'0609608648016503040203' - ); - if(!array_key_exists($hashAlg, $hexOidHashAlgos)) { - return false; - } - $hash = hash($hashAlg, $binaryData); - $tsReqData = asn1::seq( - asn1::int(1). - asn1::seq( - asn1::seq($hexOidHashAlgos[$hashAlg]."0500"). // object OBJ $hexOidHashAlgos[$hashAlg] & OBJ_null - asn1::oct($hash) - ). - asn1::int(hash('crc32', rand()).'001'). // tsa nonce - '0101ff' // req return cert - ); - return hex2bin($tsReqData); - } - - /** - * Calculate 32bit (8 hex) openssl subject hash old and new - * @param string $hex_subjSequence hex subject name sequence - * @return array subject hash old and new - */ - private static function opensslSubjHash($hex_subjSequence){ - $parse = asn1::parse($hex_subjSequence,3); - $hex_subjSequence_new=''; - foreach($parse[0] as $k=>$v) { - if(is_numeric($k)) { - $hex_subjSequence_new .= asn1::set( - asn1::seq( - $v[0][0]['hexdump']. - asn1::utf8(strtolower(hex2bin($v[0][1]['value_hex']))) - ) - ); +class x509 +{ + /* + * create tsa request/query with nonce and cert req extension + * @param string $binaryData raw/binary data of tsa query + * @param string $hashAlg hash Algorithm + * @return string binary tsa query + * @public + */ + public static function tsa_query($binaryData, $hashAlg = 'sha256'): false|string + { + $hashAlg = strtolower((string) $hashAlg); + $hexOidHashAlgos = [ + 'md2' => '06082A864886F70D0202', + 'md4' => '06082A864886F70D0204', + 'md5' => '06082A864886F70D0205', + 'sha1' => '06052B0E03021A', + 'sha224' => '0609608648016503040204', + 'sha256' => '0609608648016503040201', + 'sha384' => '0609608648016503040202', + 'sha512' => '0609608648016503040203', + ]; + if (! array_key_exists($hashAlg, $hexOidHashAlgos)) { + return false; } + + $hash = hash($hashAlg, (string) $binaryData); + $tsReqData = asn1::seq( + asn1::int(1) . + asn1::seq( + asn1::seq($hexOidHashAlgos[$hashAlg] . '0500') . // object OBJ $hexOidHashAlgos[$hashAlg] & OBJ_null + asn1::oct($hash) + ) . + asn1::int(hash('crc32', random_int(0, mt_getrandmax())) . '001') . // tsa nonce + '0101ff' // req return cert + ); + + return hex2bin($tsReqData); } - $tohash = pack("H*", $hex_subjSequence_new); - $openssl_subjHash_new = hash('sha1', $tohash); - $openssl_subjHash_new = substr($openssl_subjHash_new, 0, 8); - $openssl_subjHash_new = str_split($openssl_subjHash_new, 2); - $openssl_subjHash_new = array_reverse($openssl_subjHash_new); - $openssl_subjHash_new = implode("", $openssl_subjHash_new); - $openssl_subjHash_old = hash('md5', hex2bin($hex_subjSequence)); - $openssl_subjHash_old = substr($openssl_subjHash_old, 0, 8); - $openssl_subjHash_old = str_split($openssl_subjHash_old, 2); - $openssl_subjHash_old = array_reverse($openssl_subjHash_old); - $openssl_subjHash_old = implode("", $openssl_subjHash_old); - return array( - "old"=>$openssl_subjHash_old, - "new"=>$openssl_subjHash_new - ); - } - - /** - * Parsing ocsp response data - * @param string $binaryOcspResp binary ocsp response - * @return array ocsp response structure - */ - public static function ocsp_response_parse($binaryOcspResp, &$status='') { - $hex = current(unpack("H*", $binaryOcspResp)); - $parse = asn1::parse($hex,10); - if($parse[0]['type'] == '30') { - $ocsp = $parse[0]; - } else { - return false; - } - foreach($ocsp as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '0a') { - $ocsp['responseStatus']=$value['value_hex']; - unset($ocsp[$key]); - } - if($value['type'] == 'a0') { - $ocsp['responseBytes']=$value; - unset($ocsp[$key]); - } - } else { - unset($ocsp['depth']); - unset($ocsp['type']); - unset($ocsp['typeName']); - unset($ocsp['value_hex']); - } - } - //OCSPResponseStatus ::= ENUMERATED - // successful (0), --Response has valid confirmations - // malformedRequest (1), --Illegal confirmation request - // internalError (2), --Internal error in issuer - // tryLater (3), --Try again later - // --(4) is not used - // sigRequired (5), --Must sign the request - // unauthorized (6) --Request unauthorized - if(@$ocsp['responseStatus'] != '00') { - $responseStatus['01']='malformedRequest'; - $responseStatus['02']='internalError'; - $responseStatus['03']='tryLater'; - $responseStatus['05']='sigRequired'; - $responseStatus['06']='unauthorized'; - $status = @$responseStatus[$ocsp['responseStatus']]; - return false; - } - if(!@$curr = $ocsp['responseBytes']) { - return false; - } - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30') { - $curr['responseType']=self::oidfromhex($value[0]['value_hex']); - $curr['response']=$value[1]; - unset($curr[$key]); - } - } else { - unset($curr['typeName']); - unset($curr['type']); - unset($curr['depth']); - } - } - $ocsp['responseBytes'] = $curr; - $curr = $ocsp['responseBytes']['response']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30') { - $curr['BasicOCSPResponse']=$value; - unset($curr[$key]); - } - } else { - unset($curr['typeName']); - unset($curr['type']); - unset($curr['depth']); - } - } - $ocsp['responseBytes']['response'] = $curr; - $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30' && !array_key_exists('tbsResponseData', $curr)) { - $curr['tbsResponseData']=$value; - unset($curr[$key]); - continue; - } - if($value['type'] == '30' && !array_key_exists('signatureAlgorithm', $curr)) { - $curr['signatureAlgorithm']=$value[0]['value_hex']; - unset($curr[$key]); - continue; - } - if($value['type'] == '03') { - $curr['signature']=substr($value['value_hex'], 2); - unset($curr[$key]); - } - if($value['type'] == 'a0') { - foreach($value[0] as $certsK=>$certsV) { - if(is_numeric($certsK)) { - $certs[$certsK] = $certsV['value_hex']; + + /** + * Parsing ocsp response data + * + * @param string $binaryOcspResp binary ocsp response + * + * @return array ocsp response structure + */ + public static function ocsp_response_parse(string $binaryOcspResp, &$status = '') + { + $hex = current(unpack('H*', $binaryOcspResp)); + $parse = asn1::parse($hex, 10); + if ($parse[0]['type'] == '30') { + $ocsp = $parse[0]; + } else { + return false; + } + + foreach ($ocsp as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] === '0a') { + $ocsp['responseStatus'] = $value['value_hex']; + unset($ocsp[$key]); + } + + if ($value['type'] === 'a0') { + $ocsp['responseBytes'] = $value; + unset($ocsp[$key]); + } + } else { + unset($ocsp['depth']); + unset($ocsp['type']); + unset($ocsp['typeName']); + unset($ocsp['value_hex']); } - } - $curr['certs']=$certs; - unset($curr[$key]); - } - } else { - unset($curr['typeName']); - unset($curr['type']); - unset($curr['depth']); - } - } - $ocsp['responseBytes']['response']['BasicOCSPResponse'] = $curr; - $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == 'a0') { - $curr['version']=$value[0]['value']; - unset($curr[$key]); - } - if($value['type'] == 'a1' && !array_key_exists('responderID', $curr)) { - $curr['responderID']=$value; - unset($curr[$key]); - } - if($value['type'] == 'a2') { - $curr['responderID']=$value; - unset($curr[$key]); - } - if($value['type'] == '18') { - $curr['producedAt']=$value['value']; - unset($curr[$key]); - } - if($value['type'] == '30') { - $curr['responses']=$value; - unset($curr[$key]); - } - if($value['type'] == 'a1') { - $curr['responseExtensions']=$value; - unset($curr[$key]); - } - } else { - unset($curr['typeName']); - unset($curr['type']); - unset($curr['depth']); - } - } - $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData'] = $curr; - $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30') { - $curr['lists']=$value; - unset($curr[$key]); - } - } else { - unset($curr['typeName']); - unset($curr['type']); - unset($curr['depth']); - } - } - $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions'] = $curr; - $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions']['lists']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30') { - if($value[0]['value_hex'] == '2b0601050507300102') { // nonce - $curr['nonce']=$value[0]['value_hex']; - } else { - $curr[$value[0]['value_hex']]=$value[1]; - } - unset($curr[$key]); - } - } else { - unset($curr['typeName']); - unset($curr['type']); - unset($curr['depth']); - } - } - $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions']['lists'] = $curr; - $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responses']; - $i=0; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - foreach($value as $SingleResponseK=>$SingleResponseV) { - if(is_numeric($SingleResponseK)) { - if($SingleResponseK == 0) { - foreach($SingleResponseV as $certIDk=>$certIDv) { - if(is_numeric($certIDk)) { - if($certIDv['type'] == '30') { - $certID['hashAlgorithm'] = $certIDv[0]['value_hex']; - } - if($certIDv['type'] == '04' && !array_key_exists('issuerNameHash', $certID)) { - $certID['issuerNameHash'] = $certIDv['value_hex']; + } + + //OCSPResponseStatus ::= ENUMERATED + // successful (0), --Response has valid confirmations + // malformedRequest (1), --Illegal confirmation request + // internalError (2), --Internal error in issuer + // tryLater (3), --Try again later + // --(4) is not used + // sigRequired (5), --Must sign the request + // unauthorized (6) --Request unauthorized + if (@$ocsp['responseStatus'] != '00') { + $responseStatus['01'] = 'malformedRequest'; + $responseStatus['02'] = 'internalError'; + $responseStatus['03'] = 'tryLater'; + $responseStatus['05'] = 'sigRequired'; + $responseStatus['06'] = 'unauthorized'; + $status = @$responseStatus[$ocsp['responseStatus']]; + + return false; + } + + if (! @$curr = $ocsp['responseBytes']) { + return false; + } + + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30') { + $curr['responseType'] = self::oidfromhex($value[0]['value_hex']); + $curr['response'] = $value[1]; + unset($curr[$key]); + } + } else { + unset($curr['typeName']); + unset($curr['type']); + unset($curr['depth']); + } + } + + $ocsp['responseBytes'] = $curr; + $curr = $ocsp['responseBytes']['response']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30') { + $curr['BasicOCSPResponse'] = $value; + unset($curr[$key]); + } + } else { + unset($curr['typeName']); + unset($curr['type']); + unset($curr['depth']); + } + } + + $ocsp['responseBytes']['response'] = $curr; + $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30' && ! array_key_exists('tbsResponseData', $curr)) { + $curr['tbsResponseData'] = $value; + unset($curr[$key]); + continue; + } + + if ($value['type'] == '30' && ! array_key_exists('signatureAlgorithm', $curr)) { + $curr['signatureAlgorithm'] = $value[0]['value_hex']; + unset($curr[$key]); + continue; + } + + if ($value['type'] == '03') { + $curr['signature'] = substr((string) $value['value_hex'], 2); + unset($curr[$key]); + } + + if ($value['type'] === 'a0') { + foreach ($value[0] as $certsK => $certsV) { + if (is_numeric($certsK)) { + $certs[$certsK] = $certsV['value_hex']; + } } - if($certIDv['type'] == '04') { - $certID['issuerKeyHash'] = $certIDv['value_hex']; + + $curr['certs'] = $certs; + unset($curr[$key]); + } + } else { + unset($curr['typeName']); + unset($curr['type']); + unset($curr['depth']); + } + } + + $ocsp['responseBytes']['response']['BasicOCSPResponse'] = $curr; + $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] === 'a0') { + $curr['version'] = $value[0]['value']; + unset($curr[$key]); + } + + if ($value['type'] === 'a1' && ! array_key_exists('responderID', $curr)) { + $curr['responderID'] = $value; + unset($curr[$key]); + } + + if ($value['type'] === 'a2') { + $curr['responderID'] = $value; + unset($curr[$key]); + } + + if ($value['type'] == '18') { + $curr['producedAt'] = $value['value']; + unset($curr[$key]); + } + + if ($value['type'] == '30') { + $curr['responses'] = $value; + unset($curr[$key]); + } + + if ($value['type'] === 'a1') { + $curr['responseExtensions'] = $value; + unset($curr[$key]); + } + } else { + unset($curr['typeName']); + unset($curr['type']); + unset($curr['depth']); + } + } + + $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData'] = $curr; + $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30') { + $curr['lists'] = $value; + unset($curr[$key]); + } + } else { + unset($curr['typeName']); + unset($curr['type']); + unset($curr['depth']); + } + } + + $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions'] = $curr; + $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions']['lists']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30') { + if ($value[0]['value_hex'] === '2b0601050507300102') { // nonce + $curr['nonce'] = $value[0]['value_hex']; + } else { + $curr[$value[0]['value_hex']] = $value[1]; } - if($certIDv['type'] == '02') { - $certID['serialNumber'] = $certIDv['value_hex']; + + unset($curr[$key]); + } + } else { + unset($curr['typeName']); + unset($curr['type']); + unset($curr['depth']); + } + } + + $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responseExtensions']['lists'] = $curr; + $curr = $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responses']; + $i = 0; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + foreach ($value as $SingleResponseK => $SingleResponseV) { + if (is_numeric($SingleResponseK)) { + if ($SingleResponseK == 0) { + foreach ($SingleResponseV as $certIDk => $certIDv) { + if (is_numeric($certIDk)) { + if ($certIDv['type'] == '30') { + $certID['hashAlgorithm'] = $certIDv[0]['value_hex']; + } + + if ($certIDv['type'] == '04' && ! array_key_exists('issuerNameHash', $certID)) { + $certID['issuerNameHash'] = $certIDv['value_hex']; + } + + if ($certIDv['type'] == '04') { + $certID['issuerKeyHash'] = $certIDv['value_hex']; + } + + if ($certIDv['type'] == '02') { + $certID['serialNumber'] = $certIDv['value_hex']; + } + } + } + + $cert['certID'] = $certID; + } + + if ($SingleResponseK == 1) { + if ($SingleResponseV['type'] == '82') { + $certStatus = 'unknown'; + } elseif ($SingleResponseV['type'] == '80') { + $certStatus = 'valid'; + } else { + $certStatus = 'revoked'; + } + + $cert['certStatus'] = $certStatus; + } + + if ($SingleResponseK == 2) { + $cert['thisUpdate'] = $SingleResponseV['value']; + } + + if ($SingleResponseK == 3) { + $cert['nextUpdate'] = $SingleResponseV[0]['value']; + } + + if ($SingleResponseK == 4) { + $cert['singleExtensions'] = $SingleResponseV; + } } - } - } - $cert['certID'] = $certID; - } - if($SingleResponseK == 1) { - if($SingleResponseV['type'] == '82') { - $certStatus = 'unknown'; - } elseif($SingleResponseV['type'] == '80') { - $certStatus = 'valid'; - } else { - $certStatus = 'revoked'; - } - $cert['certStatus'] = $certStatus; - } - if($SingleResponseK == 2) { - $cert['thisUpdate'] = $SingleResponseV['value']; - } - if($SingleResponseK == 3) { - $cert['nextUpdate'] = $SingleResponseV[0]['value']; - } - if($SingleResponseK == 4) { - $cert['singleExtensions'] = $SingleResponseV; - } + } + + $curr[$i] = $cert; + } else { + unset($curr[$key]); + unset($curr['typeName']); + unset($curr['type']); + unset($curr['depth']); } - } - $curr[$i] = $cert; - } else { - unset($curr[$key]); - unset($curr['typeName']); - unset($curr['type']); - unset($curr['depth']); - } - } - $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responses'] = $curr; - $arrModel = array( - 'responseStatus'=>'', - 'responseBytes'=>array( - 'response'=>'', - 'responseType'=>'' - ) - ); - $differ=array_diff_key($arrModel,$ocsp); - if(count($differ) == 0) { - $differ=array_diff_key($arrModel['responseBytes'],$ocsp['responseBytes']); - if(count($differ) > 0) { - foreach($differ as $key=>$val) { } - return false; - } - } else { - foreach($differ as $key=>$val) { - } - return false; - } - return $ocsp; - } - - /** - * Create ocsp request - * @param string $serialNumber serial number to check - * @param string $issuerNameHash sha1 hex form of issuer subject hash - * @param string $issuerKeyHash sha1 hex form of issuer subject public info hash - * @param string $signer_cert cert to sign ocsp request - * @param string $signer_key privkey to sign ocsp request - * @param string $subjectName hex form of asn1 subject - * @return string hex form ocsp request - */ - public static function ocsp_request($serialNumber, $issuerNameHash, $issuerKeyHash, $signer_cert = false, $signer_key = false, $subjectName=false) { - $Request = false; - $hashAlgorithm = asn1::seq( - "06052B0E03021A". // OBJ_sha1 - "0500" - ); - $issuerNameHash = asn1::oct($issuerNameHash); - $issuerKeyHash = asn1::oct($issuerKeyHash); - $serialNumber = asn1::int($serialNumber); - $CertID = asn1::seq($hashAlgorithm.$issuerNameHash.$issuerKeyHash.$serialNumber); - $Request = asn1::seq($CertID); // one request - if($signer_cert) { - $requestorName = asn1::expl("1", asn1::expl("4", $subjectName)); - } else { - $requestorName = false; - } - $requestList = asn1::seq($Request); // add more request into sequence - $rand = microtime (true)*rand(); - $nonce = md5(base64_encode($rand).$rand); - $ReqExts = asn1::seq( - '06092B0601050507300102'. // OBJ_id_pkix_OCSP_Nonce - asn1::oct("0410".$nonce) - ); - $requestExtensions = asn1::expl( "2", asn1::seq($ReqExts)); - $TBSRequest = asn1::seq($requestorName.$requestList.$requestExtensions); - $optionalSignature = ''; - if($signer_cert) { - if(!openssl_sign (hex2bin($TBSRequest), $signature_value, $signer_key)) { - return false; - } - $signatureAlgorithm = asn1::seq( - '06092A864886F70D010105'. // OBJ_sha1WithRSAEncryption. - "0500" - ); - $signature = asn1::bit("00".bin2hex($signature_value)); - $signer_cert = x509::x509_pem2der($signer_cert); - $certs = asn1::expl("0", asn1::seq(bin2hex($signer_cert))); - $optionalSignature = asn1::expl("0",asn1::seq($signatureAlgorithm.$signature.$certs)); - } - $OCSPRequest = asn1::seq($TBSRequest.$optionalSignature); - return $OCSPRequest; - } - - /** - * Convert crl from pem to der (binary) - * @param string $crl pem crl to convert - * @return string der crl form - */ - public static function crl_pem2der($crl) { - $begin = '-----BEGIN X509 CRL-----'; - $end = '-----END X509 CRL-----'; - $beginPos = stripos($crl, $begin); - if($beginPos===false) { - return false; - } - $crl = substr($crl, $beginPos+strlen($begin)); - $endPos = stripos($crl, $end); - if($endPos===false) { - return false; - } - $crl = substr($crl, 0, $endPos); - $crl = str_replace("\n", "", $crl); - $crl = str_replace("\r", "", $crl); - $dercrl = base64_decode($crl); - return $dercrl; - } - - /** - * Read crl from pem or der (binary) - * @param string $crl pem or der crl - * @return array der crl and parsed crl - */ - public static function crl_read($crl) { - if(!$crlparse=self::parsecrl($crl)) { // if cant read, thats not crl - return false; - } - if(!$dercrl=self::crl_pem2der($crl)) { // if not pem, thats already der - $dercrl=$crl; - } - $res['der'] = $dercrl; - $res['parse'] = $crlparse; - return $res; - } - - /** - * parsing crl from pem or der (binary) - * @param string $crl pem or der crl - * @param string $oidprint option show obj as hex/oid - * @return array parsed crl - */ - private static function parsecrl($crl, $oidprint = false) { - if($derCrl = self::crl_pem2der($crl)) { - $derCrl = bin2hex($derCrl); - } else { - $derCrl = bin2hex($crl); - } - $curr = asn1::parse($derCrl, 7); - foreach($curr as $key=>$value) { - if($value['type'] == '30') { - $curr['crl']=$curr[$key]; - unset($curr[$key]); - } - } - $ar=$curr; - if(!array_key_exists('crl', $ar)) { - return false; - } - $curr = $ar['crl']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30' && !array_key_exists('TBSCertList', $curr)) { - $curr['TBSCertList']=$curr[$key]; - unset($curr[$key]); - } - if($value['type'] == '30') { - $curr['signatureAlgorithm']=self::oidfromhex($value[0]['value_hex']); - unset($curr[$key]); - } - if($value['type'] == '03') { - $curr['signature']=substr($value['value'], 2); - unset($curr[$key]); - } - } else { - unset($curr[$key]); - } - } - $ar['crl'] = $curr; - $curr = $ar['crl']['TBSCertList']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '02') { - $curr['version']=$curr[$key]['value']; - unset($curr[$key]); - } - if($value['type'] == '30' && !array_key_exists('signature', $curr)) { - $curr['signature']=$value[0]['value_hex']; - unset($curr[$key]); - continue; - } - if($value['type'] == '30' && !array_key_exists('issuer', $curr)) { - $curr['issuer']=$value; - unset($curr[$key]); - continue; - } - if($value['type'] == '17' && !array_key_exists('thisUpdate', $curr)) { - $curr['thisUpdate']=hex2bin($value['value_hex']); - unset($curr[$key]); - continue; - } - if($value['type'] == '17' && !array_key_exists('nextUpdate', $curr)) { - $curr['nextUpdate']=hex2bin($value['value_hex']); - unset($curr[$key]); - continue; - } - if($value['type'] == '30' && !array_key_exists('revokedCertificates', $curr)) { - $curr['revokedCertificates']=$value; - unset($curr[$key]); - continue; - } - if($value['type'] == 'a0') { - $curr['crlExtensions']=$curr[$key]; - unset($curr[$key]); - } - } else { - unset($curr[$key]); - } - } - $ar['crl']['TBSCertList'] = $curr; - if(array_key_exists('revokedCertificates', $curr)) { - $curr = $ar['crl']['TBSCertList']['revokedCertificates']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30') { - $serial = $value[0]['value']; - $revoked['time']=hex2bin($value[1]['value_hex']); - $lists[$serial]=$revoked; - unset($curr[$key]); - } + + $ocsp['responseBytes']['response']['BasicOCSPResponse']['tbsResponseData']['responses'] = $curr; + $arrModel = [ + 'responseStatus' => '', + 'responseBytes' => [ + 'response' => '', + 'responseType' => '', + ], + ]; + $differ = array_diff_key($arrModel, $ocsp); + if (count($differ) == 0) { + $differ = array_diff_key($arrModel['responseBytes'], $ocsp['responseBytes']); + if ($differ !== []) { + return false; + } } else { - unset($curr['depth']); - unset($curr['type']); - unset($curr['typeName']); + return false; } - } - $curr['lists'] = $lists; - $ar['crl']['TBSCertList']['revokedCertificates'] = $curr; + + return $ocsp; } - if(array_key_exists('crlExtensions', $ar['crl']['TBSCertList'])) { - $curr = $ar['crl']['TBSCertList']['crlExtensions'][0]; - unset($ar['crl']['TBSCertList']['crlExtensions']); - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - $attributes_name = self::oidfromhex($value[0]['value_hex']); - if($oidprint == 'oid') { - $attributes_name = self::oidfromhex($value[0]['value_hex']); - } - if($oidprint == 'hex') { - $attributes_name = $value[0]['value_hex']; - } - $attributes_oid = self::oidfromhex($value[0]['value_hex']); - if($value['type'] == '30') { - $crlExtensionsValue = $value[1][0]; - if($attributes_oid == '2.5.29.20') { // OBJ_crl_number - $crlExtensionsValue = $crlExtensionsValue['value']; - } - if($attributes_oid == '2.5.29.35') { // OBJ_authority_key_identifier - foreach($crlExtensionsValue as $authority_key_identifierValueK=>$authority_key_identifierV) { - if(is_numeric($authority_key_identifierValueK)) { - if($authority_key_identifierV['type'] == '80') { - $authority_key_identifier['keyIdentifier'] = $authority_key_identifierV['value_hex']; - } - if($authority_key_identifierV['type'] == 'a1') { - $authority_key_identifier['authorityCertIssuer'] = $authority_key_identifierV['value_hex']; - } - if($authority_key_identifierV['type'] == '82') { - $authority_key_identifier['authorityCertSerialNumber'] = $authority_key_identifierV['value_hex']; - } - } - } - $crlExtensionsValue = $authority_key_identifier; + + /** + * Create ocsp request + * + * @param string $serialNumber serial number to check + * @param string $issuerNameHash sha1 hex form of issuer subject hash + * @param string $issuerKeyHash sha1 hex form of issuer subject public info hash + * @param bool|string $signer_cert cert to sign ocsp request + * @param bool|string $signer_key privkey to sign ocsp request + * @param bool|string $subjectName hex form of asn1 subject + * + * @return string hex form ocsp request + */ + public static function ocsp_request(string $serialNumber, string $issuerNameHash, string $issuerKeyHash, bool|string $signer_cert = false, bool|string $signer_key = false, bool|string $subjectName = false) + { + $hashAlgorithm = asn1::seq( + '06052B0E03021A0500' + ); + $issuerNameHash = asn1::oct($issuerNameHash); + $issuerKeyHash = asn1::oct($issuerKeyHash); + $serialNumber = asn1::int($serialNumber); + $CertID = asn1::seq($hashAlgorithm . $issuerNameHash . $issuerKeyHash . $serialNumber); + $Request = asn1::seq($CertID); // one request + if ($signer_cert) { + $requestorName = asn1::expl('1', asn1::expl('4', $subjectName)); + } else { + $requestorName = false; + } + + $requestList = asn1::seq($Request); // add more request into sequence + $rand = microtime(true) * random_int(0, mt_getrandmax()); + $nonce = md5(base64_encode($rand) . $rand); + $ReqExts = asn1::seq( + '06092B0601050507300102' . // OBJ_id_pkix_OCSP_Nonce + asn1::oct('0410' . $nonce) + ); + $requestExtensions = asn1::expl('2', asn1::seq($ReqExts)); + $TBSRequest = asn1::seq($requestorName . $requestList . $requestExtensions); + $optionalSignature = ''; + if ($signer_cert) { + if (! openssl_sign(hex2bin($TBSRequest), $signature_value, $signer_key)) { + return false; } - $attribute_list=$crlExtensionsValue; - } - $ar['crl']['TBSCertList']['crlExtensions'][$attributes_name] = $attribute_list; + + $signatureAlgorithm = asn1::seq( + '06092A864886F70D0101050500' + ); + $signature = asn1::bit('00' . bin2hex($signature_value)); + $signer_cert = self::x509_pem2der($signer_cert); + $certs = asn1::expl('0', asn1::seq(bin2hex($signer_cert))); + $optionalSignature = asn1::expl('0', asn1::seq($signatureAlgorithm . $signature . $certs)); } - } - } - $curr = $ar['crl']['TBSCertList']['issuer']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '31') { - if($oidprint == 'oid') { - $subjOID = self::oidfromhex($curr[$key][0][0]['value_hex']); - } elseif($oidprint == 'hex') { - $subjOID = $curr[$key][0][0]['value_hex']; - } else { - $subjOID = self::oidfromhex($curr[$key][0][0]['value_hex']); - } - $curr[$subjOID][]=hex2bin($curr[$key][0][1]['value_hex']); - unset($curr[$key]); - - } - } else { - unset($curr['depth']); - unset($curr['type']); - unset($curr['typeName']); - if($key == 'hexdump') { - $curr['sha1']=hash('sha1', pack("H*", $value)); - } - } + + return asn1::seq($TBSRequest . $optionalSignature); } - $ar['crl']['TBSCertList']['issuer'] = $curr; - $arrModel['TBSCertList']['version'] = ''; - $arrModel['TBSCertList']['signature'] = ''; - $arrModel['TBSCertList']['issuer'] = ''; - $arrModel['TBSCertList']['thisUpdate'] = ''; - $arrModel['TBSCertList']['nextUpdate'] = ''; - $arrModel['signatureAlgorithm'] = ''; - $arrModel['signature'] = ''; - $crl = $ar['crl']; - $differ=array_diff_key($arrModel,$crl); - if(count($differ) == 0) { - $differ=array_diff_key($arrModel['TBSCertList'],$crl['TBSCertList']); - if(count($differ) > 0) { - foreach($differ as $key=>$val) { + + /** + * Convert crl from pem to der (binary) + * + * @param string $crl pem crl to convert + * + * @return string der crl form + */ + public static function crl_pem2der(string $crl): false|string + { + $begin = '-----BEGIN X509 CRL-----'; + $end = '-----END X509 CRL-----'; + $beginPos = stripos($crl, $begin); + if ($beginPos === false) { + return false; } - return false; - } - } else { - foreach($differ as $key=>$val) { - } - return false; - } - return $ar['crl']; - } - - /** - * Convert x509 pem certificate to x509 der - * @param string $pem pem form cert - * @return string der form cert - */ - public static function x509_pem2der($pem) { - $x509_der = false; - if($x509_res = @openssl_x509_read($pem)) { - openssl_x509_export ($x509_res, $x509_pem); - $arr_x509_pem = explode("\n", $x509_pem); - $numarr = count($arr_x509_pem); - $i=0; - $cert_pem = false; - foreach($arr_x509_pem as $val) { - if($i > 0 && $i < ($numarr-2)) { - $cert_pem .= $val; - } - $i++; - } - $x509_der = base64_decode($cert_pem); + + $crl = substr($crl, $beginPos + strlen($begin)); + $endPos = stripos($crl, $end); + if ($endPos === false) { + return false; + } + + $crl = substr($crl, 0, $endPos); + $crl = str_replace(["\n", "\r"], '', $crl); + + return base64_decode($crl, true); } - return $x509_der; - } - - /** - * Convert x509 der certificate to x509 pem form - * @param string $der_cert der form cert - * @return string pem form cert - */ - public static function x509_der2pem($der_cert) { - $x509_pem = "-----BEGIN CERTIFICATE-----\r\n"; - $x509_pem .= chunk_split(base64_encode($der_cert),64); - $x509_pem .= "-----END CERTIFICATE-----\r\n"; - return $x509_pem; - } - - /** - * get x.509 DER/PEM Certificate and return DER encoded x.509 Certificate - * @param string $certin pem/der form cert - * @return string der form cert - */ - public static function get_cert($certin) { - if($rsccert = @openssl_x509_read ($certin)) { - openssl_x509_export ($rsccert, $cert); - return self::x509_pem2der($cert); - } else { - $pem = @self::x509_der2pem($certin); - if($rsccert = @openssl_x509_read ($pem)) { - openssl_x509_export ($rsccert, $cert); - return self::x509_pem2der($cert); - } else { - return false; - } + + /** + * Read crl from pem or der (binary) + * + * @param string $crl pem or der crl + * + * @return array der crl and parsed crl + */ + public static function crl_read(string $crl): false|array + { + if (! $crlparse = self::parsecrl($crl)) { // if cant read, thats not crl + return false; + } + + if (! $dercrl = self::crl_pem2der($crl)) { // if not pem, thats already der + $dercrl = $crl; + } + + $res['der'] = $dercrl; + $res['parse'] = $crlparse; + + return $res; } - } - - /** - * parse x.509 DER/PEM Certificate structure - * @param string $certin pem/der form cert - * @param string $oidprint show oid as oid number or hex - * @return array cert structure - */ - public static function readcert($cert_in, $oidprint=false) { - if(!$der = self::get_cert($cert_in)) { - return false; + + /** + * Convert x509 pem certificate to x509 der + * + * @param string $pem pem form cert + * + * @return string der form cert + */ + public static function x509_pem2der(string $pem): string|false + { + $x509_der = false; + if ($x509_res = @openssl_x509_read($pem)) { + openssl_x509_export($x509_res, $x509_pem); + $arr_x509_pem = explode("\n", $x509_pem); + $numarr = count($arr_x509_pem); + $i = 0; + $cert_pem = false; + foreach ($arr_x509_pem as $val) { + if ($i > 0 && $i < $numarr - 2) { + $cert_pem .= $val; + } + + $i++; + } + + $x509_der = base64_decode($cert_pem, true); + } + + return $x509_der; } - $hex = bin2hex($der); - $curr = asn1::parse($hex,10); - foreach($curr as $key=>$value) { - if($value['type'] == '30') { - $curr['cert']=$curr[$key]; - unset($curr[$key]); - } + + /** + * Convert x509 der certificate to x509 pem form + * + * @param string $der_cert der form cert + * + * @return string pem form cert + */ + public static function x509_der2pem(string $der_cert): string + { + $x509_pem = "-----BEGIN CERTIFICATE-----\r\n"; + $x509_pem .= chunk_split(base64_encode($der_cert), 64); + + return $x509_pem . "-----END CERTIFICATE-----\r\n"; } - $ar=$curr; - $curr = $ar['cert']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30' && !array_key_exists('tbsCertificate', $curr)) { - $curr['tbsCertificate']=$curr[$key]; - unset($curr[$key]); - } - if($value['type'] == '30') { - $curr['signatureAlgorithm']=self::oidfromhex($value[0]['value_hex']); - unset($curr[$key]); - } - if($value['type'] == '03') { - $curr['signatureValue']=substr($value['value'], 2); - unset($curr[$key]); - } - } else { - unset($curr[$key]); - } + + /** + * get x.509 DER/PEM Certificate and return DER encoded x.509 Certificate + * + * @param string $certin pem/der form cert + * + * @return string der form cert + */ + public static function get_cert(string $certin): string|false + { + if ($rsccert = @openssl_x509_read($certin)) { + openssl_x509_export($rsccert, $cert); + + return self::x509_pem2der($cert); + } + + $pem = @self::x509_der2pem($certin); + if ($rsccert = @openssl_x509_read($pem)) { + openssl_x509_export($rsccert, $cert); + + return self::x509_pem2der($cert); + } + + return false; + } - $ar['cert'] = $curr; - $ar['cert']['sha1Fingerprint']=hash('sha1', $der); - $curr = $ar['cert']['tbsCertificate']; - $i=0; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == 'a0') { - $curr['version']=$value[0]['value']; - unset($curr[$key]); - } - if($value['type'] == '02') { - $curr['serialNumber']=$value['value']; - unset($curr[$key]); - } - if($value['type'] == '30' && !array_key_exists('signature', $curr)) { - $curr['signature']=$value[0]['value_hex']; - unset($curr[$key]); - continue; - } - if($value['type'] == '30' && !array_key_exists('issuer', $curr)) { - foreach($value as $issuerK=>$issuerV) { - if(is_numeric($issuerK)) { - $issuerOID = $issuerV[0][0]['value_hex']; - if($oidprint == 'oid') { - $issuerOID = self::oidfromhex($issuerOID); - } elseif($oidprint == 'hex') { - } else { - $issuerOID = self::oidfromhex($issuerOID); - } - $issuer[$issuerOID][] = hex2bin($issuerV[0][1]['value_hex']); - } - } - $hexdump = $value['hexdump']; - $issuer['sha1'] = hash('sha1', hex2bin($hexdump)); - $issuer['opensslHash'] = self::opensslSubjHash($hexdump); - $issuer['hexdump'] = $hexdump; - $curr['issuer']=$issuer; - unset($curr[$key]); - continue; - } - if($value['type'] == '30' && !array_key_exists('validity', $curr)) { - $curr['validity']['notBefore']=hex2bin($value[0]['value_hex']); - $curr['validity']['notAfter']=hex2bin($value[1]['value_hex']); - unset($curr[$key]); - continue; - } - if($value['type'] == '30' && !array_key_exists('subject', $curr)) { - $asn1SubjectToHash = ''; - foreach($value as $subjectK=>$subjectV) { - if(is_numeric($subjectK)) { - $subjectOID = $subjectV[0][0]['value_hex']; - if($oidprint == 'oid') { - $subjectOID = self::oidfromhex($subjectOID); - } elseif($oidprint == 'hex') { - } else { - $subjectOID = self::oidfromhex($subjectOID); - } - $subject[$subjectOID][] = hex2bin($subjectV[0][1]['value_hex']); + + /** + * parse x.509 DER/PEM Certificate structure + * + * @param bool|string $oidprint show oid as oid number or hex + * + * @return array cert structure + */ + public static function readcert($cert_in, bool|string $oidprint = false) + { + if (! $der = self::get_cert($cert_in)) { + return false; + } + + $hex = bin2hex($der); + $curr = asn1::parse($hex, 10); + foreach ($curr as $key => $value) { + if ($value['type'] == '30') { + $curr['cert'] = $curr[$key]; + unset($curr[$key]); } - } - $hexdump = $value['hexdump']; - $subject['sha1'] = hash('sha1', hex2bin($hexdump)); - $subject['opensslHash'] = self::opensslSubjHash($hexdump); - $subject['hexdump'] = $hexdump; - $curr['subject']=$subject; - unset($curr[$key]); - continue; - } - if($value['type'] == '30' && !array_key_exists('subjectPublicKeyInfo', $curr)) { - foreach($value as $subjectPublicKeyInfoK=>$subjectPublicKeyInfoV) { - if(is_numeric($subjectPublicKeyInfoK)) { - if($subjectPublicKeyInfoV['type'] == '30') { - $subjectPublicKeyInfo['algorithm']=self::oidfromhex($subjectPublicKeyInfoV[0]['value_hex']); - } - if($subjectPublicKeyInfoV['type'] == '03') { - $subjectPublicKeyInfo['subjectPublicKey']=substr($subjectPublicKeyInfoV['value'], 2); - } + } + + $ar = $curr; + $curr = $ar['cert']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30' && ! array_key_exists('tbsCertificate', $curr)) { + $curr['tbsCertificate'] = $curr[$key]; + unset($curr[$key]); + } + + if ($value['type'] == '30') { + $curr['signatureAlgorithm'] = self::oidfromhex($value[0]['value_hex']); + unset($curr[$key]); + } + + if ($value['type'] == '03') { + $curr['signatureValue'] = substr((string) $value['value'], 2); + unset($curr[$key]); + } } else { - unset($curr[$key]); + unset($curr[$key]); } - } - $subjectPublicKeyInfo['hex']=$value['hexdump']; - $subjectPublicKey_parse =asn1::parse($subjectPublicKeyInfo['subjectPublicKey']); - $subjectPublicKeyInfo['keyLength']=(strlen(substr($subjectPublicKey_parse[0][0]['value'], 2))/2)*8; - $subjectPublicKeyInfo['sha1']=hash('sha1', pack('H*', $subjectPublicKeyInfo['subjectPublicKey'])); - $curr['subjectPublicKeyInfo']=$subjectPublicKeyInfo; - unset($curr[$key]); - continue; - } - if($value['type'] == 'a3') { - $curr['attributes']=$value[0]; - unset($curr[$key]); - } - $i++; - } else { - $tbsCertificateTag[$key]=$value; - } - } - $ar['cert']['tbsCertificate'] = $curr; - if(array_key_exists('attributes', $ar['cert']['tbsCertificate'])) { - $curr = $ar['cert']['tbsCertificate']['attributes']; - foreach($curr as $key=>$value) { - if(is_numeric($key)) { - if($value['type'] == '30') { - $critical = 0; - $extvalue = $value[1]; - $name_hex = $value[0]['value_hex']; - $value_hex = $value[1]['hexdump']; - if($value[1]['type'] == '01' && $value[1]['value_hex'] == 'ff') { - $critical = 1; - $extvalue = $value[2]; + } + + $ar['cert'] = $curr; + $ar['cert']['sha1Fingerprint'] = hash('sha1', $der); + $curr = $ar['cert']['tbsCertificate']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] === 'a0') { + $curr['version'] = $value[0]['value']; + unset($curr[$key]); + } + + if ($value['type'] == '02') { + $curr['serialNumber'] = $value['value']; + unset($curr[$key]); + } + + if ($value['type'] == '30' && ! array_key_exists('signature', $curr)) { + $curr['signature'] = $value[0]['value_hex']; + unset($curr[$key]); + continue; + } + + if ($value['type'] == '30' && ! array_key_exists('issuer', $curr)) { + foreach ($value as $issuerK => $issuerV) { + if (is_numeric($issuerK)) { + $issuerOID = $issuerV[0][0]['value_hex']; + if ($oidprint === 'oid') { + $issuerOID = self::oidfromhex($issuerOID); + } elseif ($oidprint === 'hex') { + } else { + $issuerOID = self::oidfromhex($issuerOID); + } + + $issuer[$issuerOID][] = hex2bin((string) $issuerV[0][1]['value_hex']); + } + } + + $hexdump = $value['hexdump']; + $issuer['sha1'] = hash('sha1', hex2bin((string) $hexdump)); + $issuer['opensslHash'] = self::opensslSubjHash($hexdump); + $issuer['hexdump'] = $hexdump; + $curr['issuer'] = $issuer; + unset($curr[$key]); + continue; + } + + if ($value['type'] == '30' && ! array_key_exists('validity', $curr)) { + $curr['validity']['notBefore'] = hex2bin((string) $value[0]['value_hex']); + $curr['validity']['notAfter'] = hex2bin((string) $value[1]['value_hex']); + unset($curr[$key]); + continue; + } + + if ($value['type'] == '30' && ! array_key_exists('subject', $curr)) { + foreach ($value as $subjectK => $subjectV) { + if (is_numeric($subjectK)) { + $subjectOID = $subjectV[0][0]['value_hex']; + if ($oidprint === 'oid') { + $subjectOID = self::oidfromhex($subjectOID); + } elseif ($oidprint === 'hex') { + } else { + $subjectOID = self::oidfromhex($subjectOID); + } + + $subject[$subjectOID][] = hex2bin((string) $subjectV[0][1]['value_hex']); + } + } + + $hexdump = $value['hexdump']; + $subject['sha1'] = hash('sha1', hex2bin((string) $hexdump)); + $subject['opensslHash'] = self::opensslSubjHash($hexdump); + $subject['hexdump'] = $hexdump; + $curr['subject'] = $subject; + unset($curr[$key]); + continue; + } + + if ($value['type'] == '30' && ! array_key_exists('subjectPublicKeyInfo', $curr)) { + foreach ($value as $subjectPublicKeyInfoK => $subjectPublicKeyInfoV) { + if (is_numeric($subjectPublicKeyInfoK)) { + if ($subjectPublicKeyInfoV['type'] == '30') { + $subjectPublicKeyInfo['algorithm'] = self::oidfromhex($subjectPublicKeyInfoV[0]['value_hex']); + } + + if ($subjectPublicKeyInfoV['type'] == '03') { + $subjectPublicKeyInfo['subjectPublicKey'] = substr((string) $subjectPublicKeyInfoV['value'], 2); + } + } else { + unset($curr[$key]); + } + } + + $subjectPublicKeyInfo['hex'] = $value['hexdump']; + $subjectPublicKey_parse = asn1::parse($subjectPublicKeyInfo['subjectPublicKey']); + $subjectPublicKeyInfo['keyLength'] = strlen(substr((string) $subjectPublicKey_parse[0][0]['value'], 2)) / 2 * 8; + $subjectPublicKeyInfo['sha1'] = hash('sha1', pack('H*', $subjectPublicKeyInfo['subjectPublicKey'])); + $curr['subjectPublicKeyInfo'] = $subjectPublicKeyInfo; + unset($curr[$key]); + continue; + } + + if ($value['type'] === 'a3') { + $curr['attributes'] = $value[0]; + unset($curr[$key]); + } + } else { + $tbsCertificateTag[$key] = $value; } - if($name_hex == '551d0e') { // OBJ_subject_key_identifier - $extvalue = $value[1][0]['value_hex']; + } + + $ar['cert']['tbsCertificate'] = $curr; + if (array_key_exists('attributes', $ar['cert']['tbsCertificate'])) { + $curr = $ar['cert']['tbsCertificate']['attributes']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30') { + $critical = 0; + $extvalue = $value[1]; + $name_hex = $value[0]['value_hex']; + if ($value[1]['type'] == '01' && $value[1]['value_hex'] === 'ff') { + $critical = 1; + $extvalue = $value[2]; + } + + if ($name_hex === '551d0e') { // OBJ_subject_key_identifier + $extvalue = $value[1][0]['value_hex']; + } + + if ($name_hex === '551d23') { // OBJ_authority_key_identifier + foreach ($value[1][0] as $OBJ_authority_key_identifierKey => $OBJ_authority_key_identifierVal) { + if (is_numeric($OBJ_authority_key_identifierKey)) { + if ($OBJ_authority_key_identifierVal['type'] == '80') { + $OBJ_authority_key_identifier['keyid'] = $OBJ_authority_key_identifierVal['value_hex']; + } + + if ($OBJ_authority_key_identifierVal['type'] === 'a1') { + $OBJ_authority_key_identifier['issuerName'] = $OBJ_authority_key_identifierVal['value_hex']; + } + + if ($OBJ_authority_key_identifierVal['type'] == '82') { + $OBJ_authority_key_identifier['issuerSerial'] = $OBJ_authority_key_identifierVal['value_hex']; + } + } + } + + $extvalue = $OBJ_authority_key_identifier; + } + + if ($name_hex === '2b06010505070101') { // OBJ_info_access + foreach ($value[1][0] as $OBJ_info_accessK => $OBJ_info_accessV) { + if (is_numeric($OBJ_info_accessK)) { + $OBJ_info_accessHEX = $OBJ_info_accessV[0]['value_hex']; + $OBJ_info_accessOID = self::oidfromhex($OBJ_info_accessHEX); + $OBJ_info_accessNAME = $OBJ_info_accessOID; + $OBJ_info_access[$OBJ_info_accessNAME][] = hex2bin((string) $OBJ_info_accessV[1]['value_hex']); + } + } + + $extvalue = $OBJ_info_access; + } + + if ($name_hex === '551d1f') { // OBJ_crl_distribution_points 551d1f + foreach ($value[1][0] as $OBJ_crl_distribution_pointsK => $OBJ_crl_distribution_pointsV) { + if (is_numeric($OBJ_crl_distribution_pointsK)) { + $OBJ_crl_distribution_points[] = hex2bin((string) $OBJ_crl_distribution_pointsV[0][0][0]['value_hex']); + } + } + + $extvalue = $OBJ_crl_distribution_points; + } + + if ($name_hex === '551d0f') { // OBJ_key_usage + // $extvalue = self::parse_keyUsage($extvalue[0]['value']); + } + + if ($name_hex === '551d13') { // OBJ_basic_constraints + $bc['ca'] = '0'; + $bc['pathLength'] = ''; + foreach ($extvalue[0] as $bck => $bcv) { + if (is_numeric($bck)) { + if ($bcv['type'] == '01' && $bcv['value_hex'] === 'ff') { + $bc['ca'] = '1'; + } + + if ($bcv['type'] == '02') { + $bc['pathLength'] = $bcv['value']; + } + } + } + + $extvalue = $bc; + } + + if ($name_hex === '551d25') { // OBJ_ext_key_usage 551d1f + foreach ($extvalue[0] as $OBJ_ext_key_usageK => $OBJ_ext_key_usageV) { + if (is_numeric($OBJ_ext_key_usageK)) { + $OBJ_ext_key_usageHEX = $OBJ_ext_key_usageV['value_hex']; + $OBJ_ext_key_usageOID = self::oidfromhex($OBJ_ext_key_usageHEX); + $OBJ_ext_key_usageNAME = $OBJ_ext_key_usageOID; + $OBJ_ext_key_usage[] = $OBJ_ext_key_usageNAME; + } + } + + $extvalue = $OBJ_ext_key_usage; + } + + $extsVal = [ + 'name_hex' => $value[0]['value_hex'], + 'name_oid' => self::oidfromhex($value[0]['value_hex']), + 'name' => self::oidfromhex($value[0]['value_hex']), + 'critical' => $critical, + 'value' => $extvalue, + ]; + $extNameOID = $value[0]['value_hex']; + if ($oidprint === 'oid') { + $extNameOID = self::oidfromhex($extNameOID); + } elseif ($oidprint === 'hex') { + } else { + $extNameOID = self::oidfromhex($extNameOID); + } + + $curr[$extNameOID] = $extsVal; + unset($curr[$key]); + } + } else { + unset($curr[$key]); + } + + unset($ar['cert']['tbsCertificate']['attributes']); + $ar['cert']['tbsCertificate']['attributes'] = $curr; } - if($name_hex == '551d23') { // OBJ_authority_key_identifier - foreach($value[1][0] as $OBJ_authority_key_identifierKey=>$OBJ_authority_key_identifierVal) { - if(is_numeric($OBJ_authority_key_identifierKey)) { - if($OBJ_authority_key_identifierVal['type'] == '80') { - $OBJ_authority_key_identifier['keyid'] = $OBJ_authority_key_identifierVal['value_hex']; - } - if($OBJ_authority_key_identifierVal['type'] == 'a1') { - $OBJ_authority_key_identifier['issuerName'] = $OBJ_authority_key_identifierVal['value_hex']; - } - if($OBJ_authority_key_identifierVal['type'] == '82') { - $OBJ_authority_key_identifier['issuerSerial'] = $OBJ_authority_key_identifierVal['value_hex']; - } - } - } - $extvalue = $OBJ_authority_key_identifier; + } + + return $ar['cert']; + } + + /** + * Calculate 32bit (8 hex) openssl subject hash old and new + * + * @param string $hex_subjSequence hex subject name sequence + * + * @return array subject hash old and new + */ + private static function opensslSubjHash(string $hex_subjSequence): array + { + $parse = asn1::parse($hex_subjSequence, 3); + $hex_subjSequence_new = ''; + foreach ($parse[0] as $k => $v) { + if (is_numeric($k)) { + $hex_subjSequence_new .= asn1::set( + asn1::seq( + $v[0][0]['hexdump'] . + asn1::utf8(strtolower(hex2bin((string) $v[0][1]['value_hex']))) + ) + ); } - if($name_hex == '2b06010505070101') { // OBJ_info_access - foreach($value[1][0] as $OBJ_info_accessK=>$OBJ_info_accessV) { - if(is_numeric($OBJ_info_accessK)) { - $OBJ_info_accessHEX = $OBJ_info_accessV[0]['value_hex']; - $OBJ_info_accessOID = self::oidfromhex($OBJ_info_accessHEX); - $OBJ_info_accessNAME = $OBJ_info_accessOID; - $OBJ_info_access[$OBJ_info_accessNAME][] = hex2bin($OBJ_info_accessV[1]['value_hex']); - } - } - $extvalue = $OBJ_info_access; + } + + $tohash = pack('H*', $hex_subjSequence_new); + $openssl_subjHash_new = hash('sha1', $tohash); + $openssl_subjHash_new = substr($openssl_subjHash_new, 0, 8); + + $openssl_subjHash_new2 = str_split($openssl_subjHash_new, 2); + $openssl_subjHash_new2 = array_reverse($openssl_subjHash_new2); + + $openssl_subjHash_new = implode('', $openssl_subjHash_new2); + $openssl_subjHash_old = hash('md5', hex2bin($hex_subjSequence)); + $openssl_subjHash_old = substr($openssl_subjHash_old, 0, 8); + + $openssl_subjHash_old2 = str_split($openssl_subjHash_old, 2); + $openssl_subjHash_old2 = array_reverse($openssl_subjHash_old2); + + $openssl_subjHash_old = implode('', $openssl_subjHash_old2); + + return [ + 'old' => $openssl_subjHash_old, + 'new' => $openssl_subjHash_new, + ]; + } + + /** + * parsing crl from pem or der (binary) + * + * @param string $crl pem or der crl + * @param string $oidprint option show obj as hex/oid + * + * @return array parsed crl + */ + private static function parsecrl(array $crl, bool $oidprint = false): false|array + { + $derCrl = ($derCrl = self::crl_pem2der($crl)) ? bin2hex($derCrl) : bin2hex($crl); + + $curr = asn1::parse($derCrl, 7); + foreach ($curr as $key => $value) { + if ($value['type'] == '30') { + $curr['crl'] = $curr[$key]; + unset($curr[$key]); } - if($name_hex == '551d1f') { // OBJ_crl_distribution_points 551d1f - foreach($value[1][0] as $OBJ_crl_distribution_pointsK=>$OBJ_crl_distribution_pointsV) { - if(is_numeric($OBJ_crl_distribution_pointsK)) { - $OBJ_crl_distribution_points[] = hex2bin($OBJ_crl_distribution_pointsV[0][0][0]['value_hex']); + } + + $ar = $curr; + if (! array_key_exists('crl', $ar)) { + return false; + } + + $curr = $ar['crl']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30' && ! array_key_exists('TBSCertList', $curr)) { + $curr['TBSCertList'] = $curr[$key]; + unset($curr[$key]); } - } - $extvalue = $OBJ_crl_distribution_points; + + if ($value['type'] == '30') { + $curr['signatureAlgorithm'] = self::oidfromhex($value[0]['value_hex']); + unset($curr[$key]); + } + + if ($value['type'] == '03') { + $curr['signature'] = substr((string) $value['value'], 2); + unset($curr[$key]); + } + } else { + unset($curr[$key]); } - if($name_hex == '551d0f') { // OBJ_key_usage - // $extvalue = self::parse_keyUsage($extvalue[0]['value']); + } + + $ar['crl'] = $curr; + $curr = $ar['crl']['TBSCertList']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '02') { + $curr['version'] = $curr[$key]['value']; + unset($curr[$key]); + } + + if ($value['type'] == '30' && ! array_key_exists('signature', $curr)) { + $curr['signature'] = $value[0]['value_hex']; + unset($curr[$key]); + continue; + } + + if ($value['type'] == '30' && ! array_key_exists('issuer', $curr)) { + $curr['issuer'] = $value; + unset($curr[$key]); + continue; + } + + if ($value['type'] == '17' && ! array_key_exists('thisUpdate', $curr)) { + $curr['thisUpdate'] = hex2bin((string) $value['value_hex']); + unset($curr[$key]); + continue; + } + + if ($value['type'] == '17' && ! array_key_exists('nextUpdate', $curr)) { + $curr['nextUpdate'] = hex2bin((string) $value['value_hex']); + unset($curr[$key]); + continue; + } + + if ($value['type'] == '30' && ! array_key_exists('revokedCertificates', $curr)) { + $curr['revokedCertificates'] = $value; + unset($curr[$key]); + continue; + } + + if ($value['type'] === 'a0') { + $curr['crlExtensions'] = $curr[$key]; + unset($curr[$key]); + } + } else { + unset($curr[$key]); } - if($name_hex == '551d13') { // OBJ_basic_constraints - $bc['ca'] = '0'; - $bc['pathLength'] = ''; - foreach($extvalue[0] as $bck=>$bcv) { - if(is_numeric($bck)) { - if($bcv['type'] == '01') { - if($bcv['value_hex'] == 'ff') { - $bc['ca'] = '1'; + } + + $ar['crl']['TBSCertList'] = $curr; + if (array_key_exists('revokedCertificates', $curr)) { + $curr = $ar['crl']['TBSCertList']['revokedCertificates']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '30') { + $serial = $value[0]['value']; + $revoked['time'] = hex2bin((string) $value[1]['value_hex']); + $lists[$serial] = $revoked; + unset($curr[$key]); } - } - if($bcv['type'] == '02') { - $bc['pathLength'] = $bcv['value']; - } + } else { + unset($curr['depth']); + unset($curr['type']); + unset($curr['typeName']); } - } - $extvalue = $bc; } - if($name_hex == '551d25') { // OBJ_ext_key_usage 551d1f - foreach($extvalue[0] as $OBJ_ext_key_usageK=>$OBJ_ext_key_usageV) { - if(is_numeric($OBJ_ext_key_usageK)) { - $OBJ_ext_key_usageHEX = $OBJ_ext_key_usageV['value_hex']; - $OBJ_ext_key_usageOID = self::oidfromhex($OBJ_ext_key_usageHEX); - $OBJ_ext_key_usageNAME = $OBJ_ext_key_usageOID; - $OBJ_ext_key_usage[] = $OBJ_ext_key_usageNAME; - } - } - $extvalue = $OBJ_ext_key_usage; + + $curr['lists'] = $lists; + $ar['crl']['TBSCertList']['revokedCertificates'] = $curr; + } + + if (array_key_exists('crlExtensions', $ar['crl']['TBSCertList'])) { + $curr = $ar['crl']['TBSCertList']['crlExtensions'][0]; + unset($ar['crl']['TBSCertList']['crlExtensions']); + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + $attributes_name = self::oidfromhex($value[0]['value_hex']); + if ($oidprint === 'oid') { + $attributes_name = self::oidfromhex($value[0]['value_hex']); + } + + if ($oidprint === 'hex') { + $attributes_name = $value[0]['value_hex']; + } + + $attributes_oid = self::oidfromhex($value[0]['value_hex']); + if ($value['type'] == '30') { + $crlExtensionsValue = $value[1][0]; + if ($attributes_oid === '2.5.29.20') { // OBJ_crl_number + $crlExtensionsValue = $crlExtensionsValue['value']; + } + + if ($attributes_oid === '2.5.29.35') { // OBJ_authority_key_identifier + foreach ($crlExtensionsValue as $authority_key_identifierValueK => $authority_key_identifierV) { + if (is_numeric($authority_key_identifierValueK)) { + if ($authority_key_identifierV['type'] == '80') { + $authority_key_identifier['keyIdentifier'] = $authority_key_identifierV['value_hex']; + } + + if ($authority_key_identifierV['type'] === 'a1') { + $authority_key_identifier['authorityCertIssuer'] = $authority_key_identifierV['value_hex']; + } + + if ($authority_key_identifierV['type'] == '82') { + $authority_key_identifier['authorityCertSerialNumber'] = $authority_key_identifierV['value_hex']; + } + } + } + + $crlExtensionsValue = $authority_key_identifier; + } + + $attribute_list = $crlExtensionsValue; + } + + $ar['crl']['TBSCertList']['crlExtensions'][$attributes_name] = $attribute_list; + } } - $extsVal=array( - 'name_hex'=>$value[0]['value_hex'], - 'name_oid'=>self::oidfromhex($value[0]['value_hex']), - 'name'=>self::oidfromhex($value[0]['value_hex']), - 'critical'=>$critical, - 'value'=>$extvalue - ); - $extNameOID = $value[0]['value_hex']; - if($oidprint == 'oid') { - $extNameOID = self::oidfromhex($extNameOID); - } elseif($oidprint == 'hex') { + } + + $curr = $ar['crl']['TBSCertList']['issuer']; + foreach ($curr as $key => $value) { + if (is_numeric($key)) { + if ($value['type'] == '31') { + if ($oidprint === 'oid') { + $subjOID = self::oidfromhex($curr[$key][0][0]['value_hex']); + } elseif ($oidprint === 'hex') { + $subjOID = $curr[$key][0][0]['value_hex']; + } else { + $subjOID = self::oidfromhex($curr[$key][0][0]['value_hex']); + } + + $curr[$subjOID][] = hex2bin((string) $curr[$key][0][1]['value_hex']); + unset($curr[$key]); + } } else { - $extNameOID = self::oidfromhex($extNameOID); + unset($curr['depth']); + unset($curr['type']); + unset($curr['typeName']); + if ($key === 'hexdump') { + $curr['sha1'] = hash('sha1', pack('H*', $value)); + } + } + } + + $ar['crl']['TBSCertList']['issuer'] = $curr; + $arrModel['TBSCertList']['version'] = ''; + $arrModel['TBSCertList']['signature'] = ''; + $arrModel['TBSCertList']['issuer'] = ''; + $arrModel['TBSCertList']['thisUpdate'] = ''; + $arrModel['TBSCertList']['nextUpdate'] = ''; + $arrModel['signatureAlgorithm'] = ''; + $arrModel['signature'] = ''; + $crl = $ar['crl']; + $differ = array_diff_key($arrModel, $crl); + if (count($differ) == 0) { + $differ = array_diff_key($arrModel['TBSCertList'], $crl['TBSCertList']); + if ($differ !== []) { + return false; } - $curr[$extNameOID] = $extsVal; - unset($curr[$key]); - } } else { - unset($curr[$key]); + foreach ($differ as $val) { + } + + return false; } - unset($ar['cert']['tbsCertificate']['attributes']); - $ar['cert']['tbsCertificate']['attributes'] = $curr; - } - } - return $ar['cert']; - } - - /** - * read oid number of given hex (convert hex to oid) - * @param string $hex hex form oid number - * @return string oid number - */ - private static function oidfromhex($hex) { - $split = str_split($hex, 2); - $i = 0; - foreach($split as $val) { - $dec = hexdec($val); - $mplx[$i] = ($dec-128)*128; - $i++; + + return $ar['crl']; } - $i = 0; - $nex = false; - $result = false; - foreach($split as $val) { - $dec = hexdec($val); - if($i == 0) { - if($dec >= 128) { - $nex = (128*($dec-128))-80; - if($dec > 129) { - $nex = (128*($dec-128))-80; - } - $result = "2."; - } - if($dec >= 80 && $dec < 128) { - $first = $dec-80; - $result = "2.$first."; - } - if($dec >= 40 && $dec < 80) { - $first = $dec-40; - $result = "1.$first."; - } - if($dec < 40) { - $first = $dec-0; - $result = "0.$first."; - } - } else { - if($dec > 127) { - if($nex == false) { - $nex = $mplx[$i]; - } else { - $nex = ($nex*128)+$mplx[$i]; - } - } else { - $result .= ($dec+$nex)."."; - if($dec <= 127) { - $nex = 0; - } + + /** + * read oid number of given hex (convert hex to oid) + * + * @param string $hex hex form oid number + * + * @return string oid number + */ + private static function oidfromhex(string $hex): string + { + $split = str_split($hex, 2); + $i = 0; + foreach ($split as $val) { + $dec = hexdec($val); + $mplx[$i] = ($dec - 128) * 128; + $i++; } - } - $i++; + + $i = 0; + $nex = false; + $result = false; + foreach ($split as $val) { + $dec = hexdec($val); + if ($i === 0) { + if ($dec >= 128) { + $nex = 128 * ($dec - 128) - 80; + if ($dec > 129) { + $nex = 128 * ($dec - 128) - 80; + } + + $result = '2.'; + } + + if ($dec >= 80 && $dec < 128) { + $first = $dec - 80; + $result = sprintf('2.%s.', $first); + } + + if ($dec >= 40 && $dec < 80) { + $first = $dec - 40; + $result = sprintf('1.%s.', $first); + } + + if ($dec < 40) { + $first = $dec; + $result = sprintf('0.%s.', $first); + } + } elseif ($dec > 127) { + $nex = $nex == false ? $mplx[$i] : $nex * 128 + $mplx[$i]; + } else { + $result .= $dec + $nex . '.'; + if ($dec <= 127) { + $nex = 0; + } + } + + $i++; + } + + return rtrim($result, '.'); } - return rtrim($result, "."); - } } diff --git a/src/pdfvalue/PDFValue.php b/src/pdfvalue/PDFValue.php index 30ce38a..57601b0 100644 --- a/src/pdfvalue/PDFValue.php +++ b/src/pdfvalue/PDFValue.php @@ -20,64 +20,104 @@ */ namespace ddn\sapp\pdfvalue; -use \ArrayAccess; -use function ddn\sapp\helpers\p_debug_var; -class PDFValue implements ArrayAccess { - protected $value = null; - public function __construct($v) { - $this->value = $v; +use ArrayAccess; +use Exception; +use ReturnTypeWillChange; +use Stringable; + +class PDFValue implements ArrayAccess, Stringable +{ + public function __construct( + protected $value, + ) { } - public function val() { - return $this->value; + + public function __toString(): string + { + return '' . $this->value; } - public function __toString() { - return "" . $this->value; + + public function val() + { + return $this->value; } - public function offsetExists ( $offset ) : bool { - if (!is_array($this->value)) return false; + + public function offsetExists($offset): bool + { + if (! is_array($this->value)) { + return false; + } + return isset($this->value[$offset]); } - #[\ReturnTypeWillChange] - public function offsetGet ( $offset ) { - if (!is_array($this->value)) return false; - if (!isset($this->value[$offset])) return false; + + #[ReturnTypeWillChange] + public function offsetGet($offset) + { + if (! is_array($this->value)) { + return false; + } + + if (! isset($this->value[$offset])) { + return false; + } + return $this->value[$offset]; } - public function offsetSet($offset , $value ) : void { - if (!is_array($this->value)) return; + + public function offsetSet($offset, $value): void + { + if (! is_array($this->value)) { + return; + } + $this->value[$offset] = $value; } - public function offsetUnset($offset ) : void { - if ((!is_array($this->value)) || (!isset($this->value[$offset]))) + + public function offsetUnset($offset): void + { + if (! is_array($this->value) || ! isset($this->value[$offset])) { throw new Exception('invalid offset'); + } + unset($this->value[$offset]); - } - public function push($v) { + } + + public function push(mixed $v): bool + { /*if (get_class($v) !== get_class($this)) throw new Exception('invalid object to concat to this one');*/ return false; } - public function get_int() { + + public function get_int(): false|int + { return false; } - public function get_object_referenced() { + + public function get_object_referenced(): false|array|int + { return false; - } - public function get_keys() { + } + + public function get_keys(): false|array + { return false; } + /** * Returns the difference between this and other object (false means "cannot compare", null means "equal" and any value means "different": things in this object that are different from the other) */ - public function diff($other) { - if (!is_a($other, get_class($this))) - return false; + public function diff(object $other): mixed + { if ($this->value === $other->value) { return null; } + return $this->value; } + /** * Function that converts standard types into PDFValue* types * - integer, double are translated into PDFValueSimple @@ -85,46 +125,51 @@ public function diff($other) { * - string without separator (e.g. "\t\n ") are translated into PDFValueSimple * - other strings are translated into PDFValueString * - array is translated into PDFValueList, and its inner elements are also converted. + * * @param value a standard php object (e.g. string, integer, double, array, etc.) - * @return pdfvalue an object of type PDFValue*, depending on the + * + * @return pdfvalue an object of type PDFValue*, depending on the */ - protected static function _convert($value) { + protected static function _convert($value): self + { switch (gettype($value)) { case 'integer': case 'double': $value = new PDFValueSimple($value); break; case 'string': - if ($value[0] === '/') + if ($value[0] === '/') { $value = new PDFValueType(substr($value, 1)); - else - if (preg_match("/\s/ms", $value) === 1) - $value = new PDFValueString($value); - else - $value = new PDFValueSimple($value); + } elseif (preg_match("/\s/ms", $value) === 1) { + $value = new PDFValueString($value); + } else { + $value = new PDFValueSimple($value); + } + break; - case 'array': - if (count($value) === 0) { + case 'array': + if ($value === []) { // An empty list is assumed to be a list $value = new PDFValueList(); } else { - // Try to parse it as an object (i.e. [ 'Field' => 'Value', ...]) - $obj = PDFValueObject::fromarray($value); - if ($obj !== false) + $obj = PDFValueObject::fromarray($value); + if ($obj !== false) { $value = $obj; - else { - + } else { // If not an object, it is a list $list = []; foreach ($value as $v) { - array_push($list, self::_convert($v)); + $list[] = self::_convert($v); } + $value = new PDFValueList($list); } } + break; } + return $value; - } + } } diff --git a/src/pdfvalue/PDFValueHexString.php b/src/pdfvalue/PDFValueHexString.php index 101dc3d..802814f 100644 --- a/src/pdfvalue/PDFValueHexString.php +++ b/src/pdfvalue/PDFValueHexString.php @@ -21,8 +21,10 @@ namespace ddn\sapp\pdfvalue; -class PDFValueHexString extends PDFValueString { - public function __toString() { - return "<" . trim($this->value) . ">"; +class PDFValueHexString extends PDFValueString +{ + public function __toString(): string + { + return '<' . trim((string) $this->value) . '>'; } } diff --git a/src/pdfvalue/PDFValueList.php b/src/pdfvalue/PDFValueList.php index 4bb6b36..9e9fa52 100644 --- a/src/pdfvalue/PDFValueList.php +++ b/src/pdfvalue/PDFValueList.php @@ -20,86 +20,104 @@ */ namespace ddn\sapp\pdfvalue; -use function ddn\sapp\helpers\p_debug_var; -use function ddn\sapp\helpers\p_debug; -use ddn\sapp\pdfvalue\PDFValueSimple; -class PDFValueList extends PDFValue { - public function __construct($value = []) { +class PDFValueList extends PDFValue +{ + public function __construct($value = []) + { parent::__construct($value); } - public function __toString() { + + public function __toString(): string + { return '[' . implode(' ', $this->value) . ']'; } - public function diff($other) { + public function diff(object $other): mixed + { $different = parent::diff($other); - if (($different === false) || ($different === null)) return $different; + if ($different === false || $different === null) { + return $different; + } $s1 = $this->__toString(); $s2 = $other->__toString(); - if ($s1 === $s2) return null; + if ($s1 === $s2) { + return null; + } + return $this; } /** - * This function + * This function */ - public function val($list = false) { + public function val($list = false) + { if ($list === true) { $result = []; foreach ($this->value as $v) { - if (is_a($v, "ddn\\sapp\\pdfvalue\\PDFValueSimple")) { - $v = explode(" ", $v->val()); - } else { - $v = [ $v->val() ]; - } + $v = is_a($v, PDFValueSimple::class) ? explode(' ', (string) $v->val()) : [$v->val()]; + array_push($result, ...$v); } + return $result; - } else - return parent::val(); + } + + return parent::val(); + } /** * This function returns a list of objects that are referenced in the list, only if all of them are references to objects */ - public function get_object_referenced() { + public function get_object_referenced(): false|array + { $ids = []; $plain_text_val = implode(' ', $this->value); - if (trim($plain_text_val) !== "") { + if (trim($plain_text_val) !== '') { if (preg_match_all('/(([0-9]+)\s+[0-9]+\s+R)[^0-9]*/ms', $plain_text_val, $matches) > 0) { - $rebuilt = implode(" ", $matches[0]); + $rebuilt = implode(' ', $matches[0]); $rebuilt = preg_replace('/\s+/ms', ' ', $rebuilt); $plain_text_val = preg_replace('/\s+/ms', ' ', $plain_text_val); if ($plain_text_val === $rebuilt) { // Any content is a reference - foreach ($matches[2] as $id) - array_push($ids, intval($id)); - } - } else + foreach ($matches[2] as $id) { + $ids[] = (int) $id; + } + } + } else { return false; + } } + return $ids; } /** - * This method pushes the parameter to the list; - * - if it is an array, the list is merged; - * - if it is a list object, the lists are merged; + * This method pushes the parameter to the list; + * - if it is an array, the list is merged; + * - if it is a list object, the lists are merged; * - otherwise the object is converted to a PDFValue* object and it is appended to the list */ - public function push($v) { - if (is_object($v) && (get_class($v) === get_class($this))) { + public function push(mixed $v): bool + { + if (is_object($v) && $v::class === static::class) { // If a list is pushed to another list, the elements are merged $v = $v->val(); } - if (!is_array($v)) $v = [ $v ]; + + if (! is_array($v)) { + $v = [$v]; + } + foreach ($v as $e) { $e = self::_convert($e); - array_push($this->value, $e); + $this->value[] = $e; } + return true; } } diff --git a/src/pdfvalue/PDFValueObject.php b/src/pdfvalue/PDFValueObject.php index 48fcf40..78a8a82 100644 --- a/src/pdfvalue/PDFValueObject.php +++ b/src/pdfvalue/PDFValueObject.php @@ -21,132 +21,167 @@ namespace ddn\sapp\pdfvalue; -class PDFValueObject extends PDFValue { - public function __construct($value = []) { +class PDFValueObject extends PDFValue +{ + public function __construct($value = []) + { $result = []; foreach ($value as $k => $v) { $result[$k] = self::_convert($v); } + parent::__construct($result); } - public function diff($other) { + /** + * Function to output the object using the PDF format, and trying to make it compact (by reducing spaces, depending on the values) + * + * @return pdfentry the PDF entry for the object + */ + public function __toString(): string + { + $result = []; + foreach ($this->value as $k => $v) { + $v = '' . $v; + if ($v === '') { + $result[] = '/' . $k; + continue; + } + + match ($v[0]) { + '/', '[', '(', '<' => array_push($result, sprintf('/%s%s', $k, $v)), + default => array_push($result, sprintf('/%s %s', $k, $v)), + }; + } + + return '<<' . implode('', $result) . '>>'; + } + + public function diff(object $other): mixed + { $different = parent::diff($other); - if (($different === false) || ($different === null)) return $different; + if ($different === false || $different === null) { + return $different; + } - $result = new PDFValueObject(); + $result = new self(); $differences = 0; foreach ($this->value as $k => $v) { if (isset($other->value[$k])) { - if (is_a($this->value[$k], "ddn\sapp\pdfvalue\PDFValue")) { + if (is_a($this->value[$k], PDFValue::class)) { $different = $this->value[$k]->diff($other->value[$k]); if ($different === false) { $result[$k] = $v; $differences++; - } else - if ($different !== null) { + } elseif ($different !== null) { $result[$k] = $different; $differences++; - } - } + } + } } else { $result[$k] = $v; $differences++; } } - if ($differences === 0) + + if ($differences === 0) { return null; - + } + return $result; } - public static function fromarray($parts) { + public static function fromarray(array $parts): false|self + { $k = array_keys($parts); $intkeys = false; $result = []; - foreach ($k as $ck) + foreach ($k as $ck) { if (is_int($ck)) { $intkeys = true; break; } - if ($intkeys) return false; - foreach ($parts as $k => $v) { - $result[$k] = self::_convert($v); } - return new PDFValueObject($result); + + if ($intkeys) { + return false; + } + + foreach ($parts as $k2 => $v) { + $result[$k2] = self::_convert($v); + } + + return new self($result); } - public static function fromstring($str) { + public static function fromstring($str): false|self + { $result = []; $field = null; - $value = null; - $parts = explode(' ', $str); - for ($i = 0; $i < count($parts); $i++) { + $parts = explode(' ', (string) $str); + for ($i = 0, $iMax = count($parts); $i < $iMax; $i++) { if ($field === null) { $field = $parts[$i]; - if ($field === '') return false; - if ($field[0] !== '/') return false; + if ($field === '') { + return false; + } + + if ($field[0] !== '/') { + return false; + } + $field = substr($field, 1); - if ($field === '') return false; + if ($field === '') { + return false; + } + continue; } + $value = $parts[$i]; $result[$field] = $value; $field = null; } + // If there is no pair of values, there is no valid - if ($field !== null) return false; - return new PDFValueObject($result); + if ($field !== null) { + return false; + } + + return new self($result); } - public function get_keys() { + public function get_keys(): false|array + { return array_keys($this->value); } /** * Function used to enable using [x] to set values to the fields of the object (from ArrayAccess interface) * i.e. object[offset]=value + * * @param offset the index used inside the braces * @param value the value to set to that index (it will be converted to a PDFValue* object) + * * @return value the value set to the field */ - public function offsetSet($offset , $value) : void { + public function offsetSet($offset, $value): void + { if ($value === null) { - if (isset($this->value[$offset])) + if (isset($this->value[$offset])) { unset($this->value[$offset]); + } + // return null; } + $this->value[$offset] = self::_convert($value); // return $this->value[$offset]; } - public function offsetExists ( $offset ) : bool { - return isset($this->value[$offset]); - } - /** - * Function to output the object using the PDF format, and trying to make it compact (by reducing spaces, depending on the values) - * @return pdfentry the PDF entry for the object - */ - public function __toString() { - $result = []; - foreach ($this->value as $k => $v) { - $v = "" . $v; - if ($v === "") { - array_push($result, "/$k"); - continue; - } - switch ($v[0]) { - case '/': - case '[': - case '(': - case '<': - array_push($result, "/$k$v"); - break; - default: - array_push($result, "/$k $v"); - } - } - return "<<" . implode('', $result) . ">>"; + public function offsetExists($offset): bool + { + return isset($this->value[$offset]); } -} \ No newline at end of file +} diff --git a/src/pdfvalue/PDFValueReference.php b/src/pdfvalue/PDFValueReference.php index 1d7790f..bf24801 100644 --- a/src/pdfvalue/PDFValueReference.php +++ b/src/pdfvalue/PDFValueReference.php @@ -24,8 +24,10 @@ /** * Class to create a reference to an object */ -class PDFValueReference extends PDFValueSimple { - public function __construct($oid) { - parent::__construct(sprintf("%d 0 R", $oid)); +class PDFValueReference extends PDFValueSimple +{ + public function __construct($oid) + { + parent::__construct(sprintf('%d 0 R', $oid)); } -}; \ No newline at end of file +} diff --git a/src/pdfvalue/PDFValueSimple.php b/src/pdfvalue/PDFValueSimple.php index b05f7ab..048c8b7 100644 --- a/src/pdfvalue/PDFValueSimple.php +++ b/src/pdfvalue/PDFValueSimple.php @@ -21,26 +21,35 @@ namespace ddn\sapp\pdfvalue; -class PDFValueSimple extends PDFValue { - public function __construct($v) { - parent::__construct($v); - } - public function push($v) { - if (get_class($v) === get_class($this)) { +class PDFValueSimple extends PDFValue +{ + public function push($v): bool + { + if ($v::class === static::class) { // Can push - $this->value = $this->value . ' ' . $v->val(); + $this->value .= ' ' . $v->val(); + return true; } + return false; } - public function get_object_referenced() { - if (! preg_match('/^\s*([0-9]+)\s+([0-9]+)\s+R\s*$/ms', $this->value, $matches)) { + + public function get_object_referenced(): false|int + { + if (! preg_match('/^\s*([0-9]+)\s+([0-9]+)\s+R\s*$/ms', (string) $this->value, $matches)) { return false; } - return intval($matches[1]); + + return (int) $matches[1]; } - public function get_int() { - if (! is_numeric($this->value)) return false; - return intval($this->value); + + public function get_int(): false|int + { + if (! is_numeric($this->value)) { + return false; + } + + return (int) $this->value; } -} \ No newline at end of file +} diff --git a/src/pdfvalue/PDFValueString.php b/src/pdfvalue/PDFValueString.php index ae68d60..749e6b2 100644 --- a/src/pdfvalue/PDFValueString.php +++ b/src/pdfvalue/PDFValueString.php @@ -21,8 +21,10 @@ namespace ddn\sapp\pdfvalue; -class PDFValueString extends PDFValue { - public function __toString() { - return "(" . $this->value . ")"; +class PDFValueString extends PDFValue +{ + public function __toString(): string + { + return '(' . $this->value . ')'; } -} \ No newline at end of file +} diff --git a/src/pdfvalue/PDFValueType.php b/src/pdfvalue/PDFValueType.php index 3352192..30010bf 100644 --- a/src/pdfvalue/PDFValueType.php +++ b/src/pdfvalue/PDFValueType.php @@ -21,8 +21,10 @@ namespace ddn\sapp\pdfvalue; -class PDFValueType extends PDFValue { - public function __toString() { - return "/" . trim($this->value); +class PDFValueType extends PDFValue +{ + public function __toString(): string + { + return '/' . trim((string) $this->value); } }