From c09de842b636ad7533d5d3f28b1130e8972c3184 Mon Sep 17 00:00:00 2001 From: Luka Trovic Date: Wed, 31 Jul 2024 14:13:41 +0200 Subject: [PATCH 1/3] feat: PDF form extraction and filling using pdftk Signed-off-by: Luka Trovic --- appinfo/routes.php | 2 +- composer.json | 3 +- composer.lock | 2 +- composer/composer.json | 4 +- composer/composer/ClassLoader.php | 96 ++++++++++++---------- composer/composer/autoload_classmap.php | 9 ++ composer/composer/autoload_psr4.php | 2 + composer/composer/autoload_static.php | 26 +++++- lib/Controller/TemplateFieldController.php | 2 +- lib/Service/PdfService.php | 64 +++++++++++++++ lib/Service/TemplateFieldService.php | 16 +++- 11 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 lib/Service/PdfService.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 89571c6d23..49986d47a9 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -78,6 +78,6 @@ ['name' => 'Target#getPreview', 'url' => '/api/v1/targets/preview', 'verb' => 'GET'], ['name' => 'TemplateField#extractFields', 'url' => '/api/v1/template/fields/extract/{fileId}', 'verb' => 'GET'], - ['name' => 'TemplateField#fillFields', 'url' => '/api/v1/template/fields/fill', 'verb' => 'POST'], + ['name' => 'TemplateField#fillFields', 'url' => '/api/v1/template/fields/fill/{fileId}', 'verb' => 'POST'], ], ]; diff --git a/composer.json b/composer.json index 7da3c52596..abae264c16 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ }, "require": { "ext-json": "*", - "ext-simplexml": "*" + "ext-simplexml": "*", + "mikehaertl/php-pdftk": "^0.13.1" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/composer.lock b/composer.lock index 31c5064b94..9ec08f8938 100644 --- a/composer.lock +++ b/composer.lock @@ -2980,5 +2980,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/composer/composer.json b/composer/composer.json index 1c3b4f05e7..2c5ab55a04 100644 --- a/composer/composer.json +++ b/composer/composer.json @@ -6,7 +6,9 @@ }, "autoload" : { "psr-4": { - "OCA\\Richdocuments\\": "../lib/" + "OCA\\Richdocuments\\": "../lib/", + "mikehaertl\\pdftk\\": "../vendor/mikehaertl/php-pdftk/src/", + "mikehaertl\\shellcommand\\": "../vendor/mikehaertl/php-shellcommand/src/" } } } diff --git a/composer/composer/ClassLoader.php b/composer/composer/ClassLoader.php index 7824d8f7ea..a72151c77c 100644 --- a/composer/composer/ClassLoader.php +++ b/composer/composer/ClassLoader.php @@ -45,34 +45,35 @@ class ClassLoader /** @var \Closure(string):void */ private static $includeFile; - /** @var string|null */ + /** @var ?string */ private $vendorDir; // PSR-4 /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * List of PSR-0 prefixes - * - * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) - * - * @var array>> + * @var array[] + * @psalm-var array> */ private $prefixesPsr0 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr0 = array(); @@ -80,7 +81,8 @@ class ClassLoader private $useIncludePath = false; /** - * @var array + * @var string[] + * @psalm-var array */ private $classMap = array(); @@ -88,20 +90,21 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var array + * @var bool[] + * @psalm-var array */ private $missingClasses = array(); - /** @var string|null */ + /** @var ?string */ private $apcuPrefix; /** - * @var array + * @var self[] */ private static $registeredLoaders = array(); /** - * @param string|null $vendorDir + * @param ?string $vendorDir */ public function __construct($vendorDir = null) { @@ -110,7 +113,7 @@ public function __construct($vendorDir = null) } /** - * @return array> + * @return string[] */ public function getPrefixes() { @@ -122,7 +125,8 @@ public function getPrefixes() } /** - * @return array> + * @return array[] + * @psalm-return array> */ public function getPrefixesPsr4() { @@ -130,7 +134,8 @@ public function getPrefixesPsr4() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirs() { @@ -138,7 +143,8 @@ public function getFallbackDirs() } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirsPsr4() { @@ -146,7 +152,8 @@ public function getFallbackDirsPsr4() } /** - * @return array Array of classname => path + * @return string[] Array of classname => path + * @psalm-return array */ public function getClassMap() { @@ -154,7 +161,8 @@ public function getClassMap() } /** - * @param array $classMap Class to filename map + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap * * @return void */ @@ -171,25 +179,24 @@ public function addClassMap(array $classMap) * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - $paths + (array) $paths ); } @@ -198,19 +205,19 @@ public function add($prefix, $paths, $prepend = false) $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = $paths; + $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - $paths + (array) $paths ); } } @@ -219,9 +226,9 @@ public function add($prefix, $paths, $prepend = false) * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -229,18 +236,17 @@ public function add($prefix, $paths, $prepend = false) */ public function addPsr4($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - $paths + (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -250,18 +256,18 @@ public function addPsr4($prefix, $paths, $prepend = false) throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = $paths; + $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - $paths + (array) $paths ); } } @@ -270,8 +276,8 @@ public function addPsr4($prefix, $paths, $prepend = false) * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories * * @return void */ @@ -288,8 +294,8 @@ public function set($prefix, $paths) * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -475,9 +481,9 @@ public function findFile($class) } /** - * Returns the currently registered loaders keyed by their corresponding vendor directories. + * Returns the currently registered loaders indexed by their corresponding vendor directories. * - * @return array + * @return self[] */ public static function getRegisteredLoaders() { diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index d0b240e1a4..8c12933400 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -78,6 +78,7 @@ 'OCA\\Richdocuments\\Service\\FontService' => $baseDir . '/../lib/Service/FontService.php', 'OCA\\Richdocuments\\Service\\InitialStateService' => $baseDir . '/../lib/Service/InitialStateService.php', 'OCA\\Richdocuments\\Service\\RemoteOptionsService' => $baseDir . '/../lib/Service/RemoteOptionsService.php', + 'OCA\\Richdocuments\\Service\\PdfService' => $baseDir . '/../lib/Service/PdfService.php', 'OCA\\Richdocuments\\Service\\RemoteService' => $baseDir . '/../lib/Service/RemoteService.php', 'OCA\\Richdocuments\\Service\\TemplateFieldService' => $baseDir . '/../lib/Service/TemplateFieldService.php', 'OCA\\Richdocuments\\Service\\UserScopeService' => $baseDir . '/../lib/Service/UserScopeService.php', @@ -89,4 +90,12 @@ 'OCA\\Richdocuments\\TokenManager' => $baseDir . '/../lib/TokenManager.php', 'OCA\\Richdocuments\\UploadException' => $baseDir . '/../lib/UploadException.php', 'OCA\\Richdocuments\\WOPI\\Parser' => $baseDir . '/../lib/WOPI/Parser.php', + 'mikehaertl\\pdftk\\Command' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/Command.php', + 'mikehaertl\\pdftk\\DataFields' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/DataFields.php', + 'mikehaertl\\pdftk\\FdfFile' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/FdfFile.php', + 'mikehaertl\\pdftk\\InfoFields' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/InfoFields.php', + 'mikehaertl\\pdftk\\InfoFile' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/InfoFile.php', + 'mikehaertl\\pdftk\\Pdf' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/Pdf.php', + 'mikehaertl\\pdftk\\XfdfFile' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/XfdfFile.php', + 'mikehaertl\\shellcommand\\Command' => $baseDir . '/../vendor/mikehaertl/php-shellcommand/src/Command.php', ); diff --git a/composer/composer/autoload_psr4.php b/composer/composer/autoload_psr4.php index 00cdf99164..ca1a280063 100644 --- a/composer/composer/autoload_psr4.php +++ b/composer/composer/autoload_psr4.php @@ -6,5 +6,7 @@ $baseDir = $vendorDir; return array( + 'mikehaertl\\shellcommand\\' => array($baseDir . '/../vendor/mikehaertl/php-shellcommand/src'), + 'mikehaertl\\pdftk\\' => array($baseDir . '/../vendor/mikehaertl/php-pdftk/src'), 'OCA\\Richdocuments\\' => array($baseDir . '/../lib'), ); diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index 6a541215bc..f490bde907 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -7,14 +7,27 @@ class ComposerStaticInitRichdocuments { public static $prefixLengthsPsr4 = array ( - 'O' => + 'm' => + array ( + 'mikehaertl\\shellcommand\\' => 24, + 'mikehaertl\\pdftk\\' => 17, + ), + 'O' => array ( 'OCA\\Richdocuments\\' => 18, ), ); public static $prefixDirsPsr4 = array ( - 'OCA\\Richdocuments\\' => + 'mikehaertl\\shellcommand\\' => + array ( + 0 => __DIR__ . '/..' . '/../vendor/mikehaertl/php-shellcommand/src', + ), + 'mikehaertl\\pdftk\\' => + array ( + 0 => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src', + ), + 'OCA\\Richdocuments\\' => array ( 0 => __DIR__ . '/..' . '/../lib', ), @@ -93,6 +106,7 @@ class ComposerStaticInitRichdocuments 'OCA\\Richdocuments\\Service\\FontService' => __DIR__ . '/..' . '/../lib/Service/FontService.php', 'OCA\\Richdocuments\\Service\\InitialStateService' => __DIR__ . '/..' . '/../lib/Service/InitialStateService.php', 'OCA\\Richdocuments\\Service\\RemoteOptionsService' => __DIR__ . '/..' . '/../lib/Service/RemoteOptionsService.php', + 'OCA\\Richdocuments\\Service\\PdfService' => __DIR__ . '/..' . '/../lib/Service/PdfService.php', 'OCA\\Richdocuments\\Service\\RemoteService' => __DIR__ . '/..' . '/../lib/Service/RemoteService.php', 'OCA\\Richdocuments\\Service\\TemplateFieldService' => __DIR__ . '/..' . '/../lib/Service/TemplateFieldService.php', 'OCA\\Richdocuments\\Service\\UserScopeService' => __DIR__ . '/..' . '/../lib/Service/UserScopeService.php', @@ -104,6 +118,14 @@ class ComposerStaticInitRichdocuments 'OCA\\Richdocuments\\TokenManager' => __DIR__ . '/..' . '/../lib/TokenManager.php', 'OCA\\Richdocuments\\UploadException' => __DIR__ . '/..' . '/../lib/UploadException.php', 'OCA\\Richdocuments\\WOPI\\Parser' => __DIR__ . '/..' . '/../lib/WOPI/Parser.php', + 'mikehaertl\\pdftk\\Command' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/Command.php', + 'mikehaertl\\pdftk\\DataFields' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/DataFields.php', + 'mikehaertl\\pdftk\\FdfFile' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/FdfFile.php', + 'mikehaertl\\pdftk\\InfoFields' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/InfoFields.php', + 'mikehaertl\\pdftk\\InfoFile' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/InfoFile.php', + 'mikehaertl\\pdftk\\Pdf' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/Pdf.php', + 'mikehaertl\\pdftk\\XfdfFile' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/XfdfFile.php', + 'mikehaertl\\shellcommand\\Command' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-shellcommand/src/Command.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/lib/Controller/TemplateFieldController.php b/lib/Controller/TemplateFieldController.php index 69a6c8432d..be81e9b78f 100644 --- a/lib/Controller/TemplateFieldController.php +++ b/lib/Controller/TemplateFieldController.php @@ -63,7 +63,7 @@ public function extractFields(int $fileId): DataResponse { public function fillFields(int $fileId, array $fields): DataResponse { try { $this->templateFieldService->fillFields($fileId, $fields); - + return new DataResponse([], Http::STATUS_OK); } catch (\Exception $e) { return new DataResponse(["Unable to fill fields into the given file"], Http::STATUS_INTERNAL_SERVER_ERROR); diff --git a/lib/Service/PdfService.php b/lib/Service/PdfService.php new file mode 100644 index 0000000000..6945f5e051 --- /dev/null +++ b/lib/Service/PdfService.php @@ -0,0 +1,64 @@ +getStorage()->getLocalFile($file->getInternalPath()); + + try { + $pdf = new Pdf($filePath); + $fields = $pdf->getDataFields(); + $templateFields = []; + $index = 0; + foreach ($fields as $field) { + $fieldType = FieldType::tryFrom($field['FieldType']) ?? null; + + if ($fieldType === null) { + continue; + } + + $templateFields[] = new Field( + $index, + $field['FieldName'], + $fieldType, + ); + $index++; + } + return $templateFields; + } catch (\Exception $e) { + $this->logger->error('Failed to extract fields from PDF: {error}', ['error' => $e->getMessage(), 'exception' => $e]); + return []; + } + } + + public function fillFields(Node $file, array $fieldValues): void { + $filePath = $file->getStorage()->getLocalFile($file->getInternalPath()); + + try { + $pdf = new Pdf($filePath); + $pdf->fillForm($fieldValues); + $pdf->flatten(); + $pdf->saveAs($filePath); + } catch (\Exception $e) { + $this->logger->error('Failed to fill fields in PDF: {error}', ['error' => $e->getMessage(), 'exception' => $e]); + throw $e; + } + } +} diff --git a/lib/Service/TemplateFieldService.php b/lib/Service/TemplateFieldService.php index 2b9cffe4c7..620f2fddf3 100644 --- a/lib/Service/TemplateFieldService.php +++ b/lib/Service/TemplateFieldService.php @@ -8,6 +8,8 @@ namespace OCA\Richdocuments\Service; use OCA\Richdocuments\AppConfig; +use OCA\Richdocuments\Capabilities; +use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\Template\Field; @@ -23,7 +25,8 @@ public function __construct( private AppConfig $appConfig, private IRootFolder $rootFolder, private LoggerInterface $logger, - private ICacheFactory $cacheFactory + private ICacheFactory $cacheFactory, + private PdfService $pdfService, ) { } @@ -49,6 +52,12 @@ public function extractFields(Node|int $file) { return $cachedResponse; } + if ($file->getMimeType() === 'application/pdf') { + $fields = $this->pdfService->extractFields($file); + $localCache->set($cacheName, $fields, 3600); + return $fields; + } + $collaboraUrl = $this->appConfig->getCollaboraUrlInternal(); $httpClient = $this->clientService->newClient(); @@ -110,6 +119,11 @@ public function fillFields(Node|int $file, array $fields = []) { $file = $this->rootFolder->getFirstNodeById($file); } + if ($file->getMimeType() === 'application/pdf') { + $this->pdfService->fillFields($file, $fields); + return ''; + } + $collaboraUrl = $this->appConfig->getCollaboraUrlInternal(); $httpClient = $this->clientService->newClient(); From 316b380c5f3c9efccc4993cd250d25994bd7b9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 1 Aug 2024 16:13:58 +0200 Subject: [PATCH 2/3] chore: Move to actual runtime dependency file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- composer.json | 3 +- composer.lock | 2 +- composer/composer.json | 7 +- composer/composer.lock | 146 +++- composer/composer/ClassLoader.php | 96 ++- composer/composer/autoload_classmap.php | 19 +- composer/composer/autoload_psr4.php | 5 +- composer/composer/autoload_real.php | 2 + composer/composer/autoload_static.php | 38 +- composer/composer/installed.json | 151 +++- composer/composer/installed.php | 31 +- composer/composer/platform_check.php | 26 + .../php-pdftk/.github/workflows/tests.yml | 55 ++ composer/mikehaertl/php-pdftk/LICENSE | 21 + composer/mikehaertl/php-pdftk/README.md | 501 ++++++++++++ composer/mikehaertl/php-pdftk/composer.json | 31 + composer/mikehaertl/php-pdftk/src/Command.php | 249 ++++++ .../mikehaertl/php-pdftk/src/DataFields.php | 166 ++++ composer/mikehaertl/php-pdftk/src/FdfFile.php | 84 ++ .../mikehaertl/php-pdftk/src/InfoFields.php | 139 ++++ .../mikehaertl/php-pdftk/src/InfoFile.php | 190 +++++ composer/mikehaertl/php-pdftk/src/Pdf.php | 749 ++++++++++++++++++ .../mikehaertl/php-pdftk/src/XfdfFile.php | 233 ++++++ .../.github/workflows/tests.yml | 48 ++ .../mikehaertl/php-shellcommand/CHANGELOG.md | 102 +++ composer/mikehaertl/php-shellcommand/LICENSE | 21 + .../mikehaertl/php-shellcommand/README.md | 204 +++++ .../mikehaertl/php-shellcommand/composer.json | 28 + .../php-shellcommand/src/Command.php | 568 +++++++++++++ .../php-tmpfile/.github/workflows/tests.yml | 48 ++ composer/mikehaertl/php-tmpfile/LICENSE | 21 + composer/mikehaertl/php-tmpfile/README.md | 64 ++ composer/mikehaertl/php-tmpfile/composer.json | 26 + composer/mikehaertl/php-tmpfile/src/File.php | 197 +++++ 34 files changed, 4181 insertions(+), 90 deletions(-) create mode 100644 composer/composer/platform_check.php create mode 100644 composer/mikehaertl/php-pdftk/.github/workflows/tests.yml create mode 100644 composer/mikehaertl/php-pdftk/LICENSE create mode 100644 composer/mikehaertl/php-pdftk/README.md create mode 100644 composer/mikehaertl/php-pdftk/composer.json create mode 100644 composer/mikehaertl/php-pdftk/src/Command.php create mode 100644 composer/mikehaertl/php-pdftk/src/DataFields.php create mode 100644 composer/mikehaertl/php-pdftk/src/FdfFile.php create mode 100644 composer/mikehaertl/php-pdftk/src/InfoFields.php create mode 100644 composer/mikehaertl/php-pdftk/src/InfoFile.php create mode 100644 composer/mikehaertl/php-pdftk/src/Pdf.php create mode 100644 composer/mikehaertl/php-pdftk/src/XfdfFile.php create mode 100644 composer/mikehaertl/php-shellcommand/.github/workflows/tests.yml create mode 100644 composer/mikehaertl/php-shellcommand/CHANGELOG.md create mode 100644 composer/mikehaertl/php-shellcommand/LICENSE create mode 100644 composer/mikehaertl/php-shellcommand/README.md create mode 100644 composer/mikehaertl/php-shellcommand/composer.json create mode 100644 composer/mikehaertl/php-shellcommand/src/Command.php create mode 100644 composer/mikehaertl/php-tmpfile/.github/workflows/tests.yml create mode 100644 composer/mikehaertl/php-tmpfile/LICENSE create mode 100644 composer/mikehaertl/php-tmpfile/README.md create mode 100644 composer/mikehaertl/php-tmpfile/composer.json create mode 100644 composer/mikehaertl/php-tmpfile/src/File.php diff --git a/composer.json b/composer.json index abae264c16..7da3c52596 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,7 @@ }, "require": { "ext-json": "*", - "ext-simplexml": "*", - "mikehaertl/php-pdftk": "^0.13.1" + "ext-simplexml": "*" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/composer.lock b/composer.lock index 9ec08f8938..31c5064b94 100644 --- a/composer.lock +++ b/composer.lock @@ -2980,5 +2980,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/composer/composer.json b/composer/composer.json index 2c5ab55a04..b1dc9cabb3 100644 --- a/composer/composer.json +++ b/composer/composer.json @@ -6,9 +6,10 @@ }, "autoload" : { "psr-4": { - "OCA\\Richdocuments\\": "../lib/", - "mikehaertl\\pdftk\\": "../vendor/mikehaertl/php-pdftk/src/", - "mikehaertl\\shellcommand\\": "../vendor/mikehaertl/php-shellcommand/src/" + "OCA\\Richdocuments\\": "../lib/" } + }, + "require": { + "mikehaertl/php-pdftk": "^0.13.1" } } diff --git a/composer/composer.lock b/composer/composer.lock index 62a29d3672..ce76dafbf9 100644 --- a/composer/composer.lock +++ b/composer/composer.lock @@ -4,8 +4,148 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d751713988987e9331980363e24189ce", - "packages": [], + "content-hash": "b0ad6a743550ac81e64e6d88bfd62ae6", + "packages": [ + { + "name": "mikehaertl/php-pdftk", + "version": "0.13.1", + "source": { + "type": "git", + "url": "https://github.com/mikehaertl/php-pdftk.git", + "reference": "3851b08c1027489e48387d7c14c27bc295d98239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikehaertl/php-pdftk/zipball/3851b08c1027489e48387d7c14c27bc295d98239", + "reference": "3851b08c1027489e48387d7c14c27bc295d98239", + "shasum": "" + }, + "require": { + "mikehaertl/php-shellcommand": "^1.6.3", + "mikehaertl/php-tmpfile": "^1.1.0", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0 <9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "mikehaertl\\pdftk\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Haertl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "A PDF conversion and form utility based on pdftk.", + "keywords": [ + "pdf", + "pdftk" + ], + "support": { + "issues": "https://github.com/mikehaertl/php-pdftk/issues", + "source": "https://github.com/mikehaertl/php-pdftk/tree/0.13.1" + }, + "time": "2023-11-03T16:06:08+00:00" + }, + { + "name": "mikehaertl/php-shellcommand", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/mikehaertl/php-shellcommand.git", + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/e79ea528be155ffdec6f3bf1a4a46307bb49e545", + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545", + "shasum": "" + }, + "require": { + "php": ">= 5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0 <=9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "mikehaertl\\shellcommand\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "An object oriented interface to shell commands", + "keywords": [ + "shell" + ], + "support": { + "issues": "https://github.com/mikehaertl/php-shellcommand/issues", + "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.7.0" + }, + "time": "2023-04-19T08:25:22+00:00" + }, + { + "name": "mikehaertl/php-tmpfile", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/mikehaertl/php-tmpfile.git", + "reference": "70a5b70b17bc0d9666388e6a551ecc93d0b40a10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikehaertl/php-tmpfile/zipball/70a5b70b17bc0d9666388e6a551ecc93d0b40a10", + "reference": "70a5b70b17bc0d9666388e6a551ecc93d0b40a10", + "shasum": "" + }, + "require-dev": { + "php": ">=5.3.0", + "phpunit/phpunit": ">4.0 <=9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "mikehaertl\\tmp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "A convenience class for temporary files", + "keywords": [ + "files" + ], + "support": { + "issues": "https://github.com/mikehaertl/php-tmpfile/issues", + "source": "https://github.com/mikehaertl/php-tmpfile/tree/1.2.1" + }, + "time": "2021-03-01T18:26:25+00:00" + } + ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", @@ -14,5 +154,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/composer/composer/ClassLoader.php b/composer/composer/ClassLoader.php index a72151c77c..7824d8f7ea 100644 --- a/composer/composer/ClassLoader.php +++ b/composer/composer/ClassLoader.php @@ -45,35 +45,34 @@ class ClassLoader /** @var \Closure(string):void */ private static $includeFile; - /** @var ?string */ + /** @var string|null */ private $vendorDir; // PSR-4 /** - * @var array[] - * @psalm-var array> + * @var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array[] - * @psalm-var array> + * @var array> */ private $prefixDirsPsr4 = array(); /** - * @var array[] - * @psalm-var array + * @var list */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * @var array[] - * @psalm-var array> + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> */ private $prefixesPsr0 = array(); /** - * @var array[] - * @psalm-var array + * @var list */ private $fallbackDirsPsr0 = array(); @@ -81,8 +80,7 @@ class ClassLoader private $useIncludePath = false; /** - * @var string[] - * @psalm-var array + * @var array */ private $classMap = array(); @@ -90,21 +88,20 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var bool[] - * @psalm-var array + * @var array */ private $missingClasses = array(); - /** @var ?string */ + /** @var string|null */ private $apcuPrefix; /** - * @var self[] + * @var array */ private static $registeredLoaders = array(); /** - * @param ?string $vendorDir + * @param string|null $vendorDir */ public function __construct($vendorDir = null) { @@ -113,7 +110,7 @@ public function __construct($vendorDir = null) } /** - * @return string[] + * @return array> */ public function getPrefixes() { @@ -125,8 +122,7 @@ public function getPrefixes() } /** - * @return array[] - * @psalm-return array> + * @return array> */ public function getPrefixesPsr4() { @@ -134,8 +130,7 @@ public function getPrefixesPsr4() } /** - * @return array[] - * @psalm-return array + * @return list */ public function getFallbackDirs() { @@ -143,8 +138,7 @@ public function getFallbackDirs() } /** - * @return array[] - * @psalm-return array + * @return list */ public function getFallbackDirsPsr4() { @@ -152,8 +146,7 @@ public function getFallbackDirsPsr4() } /** - * @return string[] Array of classname => path - * @psalm-return array + * @return array Array of classname => path */ public function getClassMap() { @@ -161,8 +154,7 @@ public function getClassMap() } /** - * @param string[] $classMap Class to filename map - * @psalm-param array $classMap + * @param array $classMap Class to filename map * * @return void */ @@ -179,24 +171,25 @@ public function addClassMap(array $classMap) * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - (array) $paths + $paths ); } @@ -205,19 +198,19 @@ public function add($prefix, $paths, $prepend = false) $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; + $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - (array) $paths + $paths ); } } @@ -226,9 +219,9 @@ public function add($prefix, $paths, $prepend = false) * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -236,17 +229,18 @@ public function add($prefix, $paths, $prepend = false) */ public function addPsr4($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - (array) $paths + $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -256,18 +250,18 @@ public function addPsr4($prefix, $paths, $prepend = false) throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; + $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - (array) $paths + $paths ); } } @@ -276,8 +270,8 @@ public function addPsr4($prefix, $paths, $prepend = false) * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories * * @return void */ @@ -294,8 +288,8 @@ public function set($prefix, $paths) * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -481,9 +475,9 @@ public function findFile($class) } /** - * Returns the currently registered loaders indexed by their corresponding vendor directories. + * Returns the currently registered loaders keyed by their corresponding vendor directories. * - * @return self[] + * @return array */ public static function getRegisteredLoaders() { diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index 8c12933400..b630b10dcf 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -77,8 +77,8 @@ 'OCA\\Richdocuments\\Service\\FileTargetService' => $baseDir . '/../lib/Service/FileTargetService.php', 'OCA\\Richdocuments\\Service\\FontService' => $baseDir . '/../lib/Service/FontService.php', 'OCA\\Richdocuments\\Service\\InitialStateService' => $baseDir . '/../lib/Service/InitialStateService.php', - 'OCA\\Richdocuments\\Service\\RemoteOptionsService' => $baseDir . '/../lib/Service/RemoteOptionsService.php', 'OCA\\Richdocuments\\Service\\PdfService' => $baseDir . '/../lib/Service/PdfService.php', + 'OCA\\Richdocuments\\Service\\RemoteOptionsService' => $baseDir . '/../lib/Service/RemoteOptionsService.php', 'OCA\\Richdocuments\\Service\\RemoteService' => $baseDir . '/../lib/Service/RemoteService.php', 'OCA\\Richdocuments\\Service\\TemplateFieldService' => $baseDir . '/../lib/Service/TemplateFieldService.php', 'OCA\\Richdocuments\\Service\\UserScopeService' => $baseDir . '/../lib/Service/UserScopeService.php', @@ -90,12 +90,13 @@ 'OCA\\Richdocuments\\TokenManager' => $baseDir . '/../lib/TokenManager.php', 'OCA\\Richdocuments\\UploadException' => $baseDir . '/../lib/UploadException.php', 'OCA\\Richdocuments\\WOPI\\Parser' => $baseDir . '/../lib/WOPI/Parser.php', - 'mikehaertl\\pdftk\\Command' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/Command.php', - 'mikehaertl\\pdftk\\DataFields' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/DataFields.php', - 'mikehaertl\\pdftk\\FdfFile' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/FdfFile.php', - 'mikehaertl\\pdftk\\InfoFields' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/InfoFields.php', - 'mikehaertl\\pdftk\\InfoFile' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/InfoFile.php', - 'mikehaertl\\pdftk\\Pdf' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/Pdf.php', - 'mikehaertl\\pdftk\\XfdfFile' => $baseDir . '/../vendor/mikehaertl/php-pdftk/src/XfdfFile.php', - 'mikehaertl\\shellcommand\\Command' => $baseDir . '/../vendor/mikehaertl/php-shellcommand/src/Command.php', + 'mikehaertl\\pdftk\\Command' => $vendorDir . '/mikehaertl/php-pdftk/src/Command.php', + 'mikehaertl\\pdftk\\DataFields' => $vendorDir . '/mikehaertl/php-pdftk/src/DataFields.php', + 'mikehaertl\\pdftk\\FdfFile' => $vendorDir . '/mikehaertl/php-pdftk/src/FdfFile.php', + 'mikehaertl\\pdftk\\InfoFields' => $vendorDir . '/mikehaertl/php-pdftk/src/InfoFields.php', + 'mikehaertl\\pdftk\\InfoFile' => $vendorDir . '/mikehaertl/php-pdftk/src/InfoFile.php', + 'mikehaertl\\pdftk\\Pdf' => $vendorDir . '/mikehaertl/php-pdftk/src/Pdf.php', + 'mikehaertl\\pdftk\\XfdfFile' => $vendorDir . '/mikehaertl/php-pdftk/src/XfdfFile.php', + 'mikehaertl\\shellcommand\\Command' => $vendorDir . '/mikehaertl/php-shellcommand/src/Command.php', + 'mikehaertl\\tmp\\File' => $vendorDir . '/mikehaertl/php-tmpfile/src/File.php', ); diff --git a/composer/composer/autoload_psr4.php b/composer/composer/autoload_psr4.php index ca1a280063..ae29c9b417 100644 --- a/composer/composer/autoload_psr4.php +++ b/composer/composer/autoload_psr4.php @@ -6,7 +6,8 @@ $baseDir = $vendorDir; return array( - 'mikehaertl\\shellcommand\\' => array($baseDir . '/../vendor/mikehaertl/php-shellcommand/src'), - 'mikehaertl\\pdftk\\' => array($baseDir . '/../vendor/mikehaertl/php-pdftk/src'), + 'mikehaertl\\tmp\\' => array($vendorDir . '/mikehaertl/php-tmpfile/src'), + 'mikehaertl\\shellcommand\\' => array($vendorDir . '/mikehaertl/php-shellcommand/src'), + 'mikehaertl\\pdftk\\' => array($vendorDir . '/mikehaertl/php-pdftk/src'), 'OCA\\Richdocuments\\' => array($baseDir . '/../lib'), ); diff --git a/composer/composer/autoload_real.php b/composer/composer/autoload_real.php index 114f8d9322..b1646a59d0 100644 --- a/composer/composer/autoload_real.php +++ b/composer/composer/autoload_real.php @@ -22,6 +22,8 @@ public static function getLoader() return self::$loader; } + require __DIR__ . '/platform_check.php'; + spl_autoload_register(array('ComposerAutoloaderInitRichdocuments', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInitRichdocuments', 'loadClassLoader')); diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index f490bde907..f6bd8b2f9f 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -7,27 +7,32 @@ class ComposerStaticInitRichdocuments { public static $prefixLengthsPsr4 = array ( - 'm' => + 'm' => array ( + 'mikehaertl\\tmp\\' => 15, 'mikehaertl\\shellcommand\\' => 24, 'mikehaertl\\pdftk\\' => 17, ), - 'O' => + 'O' => array ( 'OCA\\Richdocuments\\' => 18, ), ); public static $prefixDirsPsr4 = array ( - 'mikehaertl\\shellcommand\\' => + 'mikehaertl\\tmp\\' => array ( - 0 => __DIR__ . '/..' . '/../vendor/mikehaertl/php-shellcommand/src', + 0 => __DIR__ . '/..' . '/mikehaertl/php-tmpfile/src', ), - 'mikehaertl\\pdftk\\' => + 'mikehaertl\\shellcommand\\' => array ( - 0 => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src', + 0 => __DIR__ . '/..' . '/mikehaertl/php-shellcommand/src', ), - 'OCA\\Richdocuments\\' => + 'mikehaertl\\pdftk\\' => + array ( + 0 => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src', + ), + 'OCA\\Richdocuments\\' => array ( 0 => __DIR__ . '/..' . '/../lib', ), @@ -105,8 +110,8 @@ class ComposerStaticInitRichdocuments 'OCA\\Richdocuments\\Service\\FileTargetService' => __DIR__ . '/..' . '/../lib/Service/FileTargetService.php', 'OCA\\Richdocuments\\Service\\FontService' => __DIR__ . '/..' . '/../lib/Service/FontService.php', 'OCA\\Richdocuments\\Service\\InitialStateService' => __DIR__ . '/..' . '/../lib/Service/InitialStateService.php', - 'OCA\\Richdocuments\\Service\\RemoteOptionsService' => __DIR__ . '/..' . '/../lib/Service/RemoteOptionsService.php', 'OCA\\Richdocuments\\Service\\PdfService' => __DIR__ . '/..' . '/../lib/Service/PdfService.php', + 'OCA\\Richdocuments\\Service\\RemoteOptionsService' => __DIR__ . '/..' . '/../lib/Service/RemoteOptionsService.php', 'OCA\\Richdocuments\\Service\\RemoteService' => __DIR__ . '/..' . '/../lib/Service/RemoteService.php', 'OCA\\Richdocuments\\Service\\TemplateFieldService' => __DIR__ . '/..' . '/../lib/Service/TemplateFieldService.php', 'OCA\\Richdocuments\\Service\\UserScopeService' => __DIR__ . '/..' . '/../lib/Service/UserScopeService.php', @@ -118,14 +123,15 @@ class ComposerStaticInitRichdocuments 'OCA\\Richdocuments\\TokenManager' => __DIR__ . '/..' . '/../lib/TokenManager.php', 'OCA\\Richdocuments\\UploadException' => __DIR__ . '/..' . '/../lib/UploadException.php', 'OCA\\Richdocuments\\WOPI\\Parser' => __DIR__ . '/..' . '/../lib/WOPI/Parser.php', - 'mikehaertl\\pdftk\\Command' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/Command.php', - 'mikehaertl\\pdftk\\DataFields' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/DataFields.php', - 'mikehaertl\\pdftk\\FdfFile' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/FdfFile.php', - 'mikehaertl\\pdftk\\InfoFields' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/InfoFields.php', - 'mikehaertl\\pdftk\\InfoFile' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/InfoFile.php', - 'mikehaertl\\pdftk\\Pdf' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/Pdf.php', - 'mikehaertl\\pdftk\\XfdfFile' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-pdftk/src/XfdfFile.php', - 'mikehaertl\\shellcommand\\Command' => __DIR__ . '/..' . '/../vendor/mikehaertl/php-shellcommand/src/Command.php', + 'mikehaertl\\pdftk\\Command' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/Command.php', + 'mikehaertl\\pdftk\\DataFields' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/DataFields.php', + 'mikehaertl\\pdftk\\FdfFile' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/FdfFile.php', + 'mikehaertl\\pdftk\\InfoFields' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/InfoFields.php', + 'mikehaertl\\pdftk\\InfoFile' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/InfoFile.php', + 'mikehaertl\\pdftk\\Pdf' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/Pdf.php', + 'mikehaertl\\pdftk\\XfdfFile' => __DIR__ . '/..' . '/mikehaertl/php-pdftk/src/XfdfFile.php', + 'mikehaertl\\shellcommand\\Command' => __DIR__ . '/..' . '/mikehaertl/php-shellcommand/src/Command.php', + 'mikehaertl\\tmp\\File' => __DIR__ . '/..' . '/mikehaertl/php-tmpfile/src/File.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/composer/composer/installed.json b/composer/composer/installed.json index 87fda747e6..d8976c6367 100644 --- a/composer/composer/installed.json +++ b/composer/composer/installed.json @@ -1,5 +1,154 @@ { - "packages": [], + "packages": [ + { + "name": "mikehaertl/php-pdftk", + "version": "0.13.1", + "version_normalized": "0.13.1.0", + "source": { + "type": "git", + "url": "https://github.com/mikehaertl/php-pdftk.git", + "reference": "3851b08c1027489e48387d7c14c27bc295d98239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikehaertl/php-pdftk/zipball/3851b08c1027489e48387d7c14c27bc295d98239", + "reference": "3851b08c1027489e48387d7c14c27bc295d98239", + "shasum": "" + }, + "require": { + "mikehaertl/php-shellcommand": "^1.6.3", + "mikehaertl/php-tmpfile": "^1.1.0", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0 <9.4" + }, + "time": "2023-11-03T16:06:08+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "mikehaertl\\pdftk\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Haertl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "A PDF conversion and form utility based on pdftk.", + "keywords": [ + "pdf", + "pdftk" + ], + "support": { + "issues": "https://github.com/mikehaertl/php-pdftk/issues", + "source": "https://github.com/mikehaertl/php-pdftk/tree/0.13.1" + }, + "install-path": "../mikehaertl/php-pdftk" + }, + { + "name": "mikehaertl/php-shellcommand", + "version": "1.7.0", + "version_normalized": "1.7.0.0", + "source": { + "type": "git", + "url": "https://github.com/mikehaertl/php-shellcommand.git", + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikehaertl/php-shellcommand/zipball/e79ea528be155ffdec6f3bf1a4a46307bb49e545", + "reference": "e79ea528be155ffdec6f3bf1a4a46307bb49e545", + "shasum": "" + }, + "require": { + "php": ">= 5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0 <=9.4" + }, + "time": "2023-04-19T08:25:22+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "mikehaertl\\shellcommand\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "An object oriented interface to shell commands", + "keywords": [ + "shell" + ], + "support": { + "issues": "https://github.com/mikehaertl/php-shellcommand/issues", + "source": "https://github.com/mikehaertl/php-shellcommand/tree/1.7.0" + }, + "install-path": "../mikehaertl/php-shellcommand" + }, + { + "name": "mikehaertl/php-tmpfile", + "version": "1.2.1", + "version_normalized": "1.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/mikehaertl/php-tmpfile.git", + "reference": "70a5b70b17bc0d9666388e6a551ecc93d0b40a10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikehaertl/php-tmpfile/zipball/70a5b70b17bc0d9666388e6a551ecc93d0b40a10", + "reference": "70a5b70b17bc0d9666388e6a551ecc93d0b40a10", + "shasum": "" + }, + "require-dev": { + "php": ">=5.3.0", + "phpunit/phpunit": ">4.0 <=9.4" + }, + "time": "2021-03-01T18:26:25+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "mikehaertl\\tmp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "description": "A convenience class for temporary files", + "keywords": [ + "files" + ], + "support": { + "issues": "https://github.com/mikehaertl/php-tmpfile/issues", + "source": "https://github.com/mikehaertl/php-tmpfile/tree/1.2.1" + }, + "install-path": "../mikehaertl/php-tmpfile" + } + ], "dev": true, "dev-package-names": [] } diff --git a/composer/composer/installed.php b/composer/composer/installed.php index afca0f5c7b..ec1eabe433 100644 --- a/composer/composer/installed.php +++ b/composer/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '54ab2a0b624b592f982ea341fe730e24dfbc4a8c', + 'reference' => '88eb9d7b4bbe5f42df84e9ed12183e91554e0a1d', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,11 +13,38 @@ '__root__' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '54ab2a0b624b592f982ea341fe730e24dfbc4a8c', + 'reference' => '88eb9d7b4bbe5f42df84e9ed12183e91554e0a1d', 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), 'dev_requirement' => false, ), + 'mikehaertl/php-pdftk' => array( + 'pretty_version' => '0.13.1', + 'version' => '0.13.1.0', + 'reference' => '3851b08c1027489e48387d7c14c27bc295d98239', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mikehaertl/php-pdftk', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'mikehaertl/php-shellcommand' => array( + 'pretty_version' => '1.7.0', + 'version' => '1.7.0.0', + 'reference' => 'e79ea528be155ffdec6f3bf1a4a46307bb49e545', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mikehaertl/php-shellcommand', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'mikehaertl/php-tmpfile' => array( + 'pretty_version' => '1.2.1', + 'version' => '1.2.1.0', + 'reference' => '70a5b70b17bc0d9666388e6a551ecc93d0b40a10', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mikehaertl/php-tmpfile', + 'aliases' => array(), + 'dev_requirement' => false, + ), ), ); diff --git a/composer/composer/platform_check.php b/composer/composer/platform_check.php new file mode 100644 index 0000000000..7621d4ff97 --- /dev/null +++ b/composer/composer/platform_check.php @@ -0,0 +1,26 @@ += 50300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 5.3.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/composer/mikehaertl/php-pdftk/.github/workflows/tests.yml b/composer/mikehaertl/php-pdftk/.github/workflows/tests.yml new file mode 100644 index 0000000000..b17b83cfd1 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +name: Tests +on: pull_request +jobs: + phpunit: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + matrix: + php: + - "5.3" + - "5.4" + - "5.5" + - "5.6" + - "7.0" + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install pdftk + run: | + cd /tmp + sudo wget http://mirrors.kernel.org/ubuntu/pool/universe/p/pdftk-java/pdftk-java_3.0.9-1_all.deb + sudo apt install -y -q ./pdftk-java_3.0.9-1_all.deb + pdftk --version + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + + - name: Update composer + run: composer self-update + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer cache + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer packages + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run phpunit + run: vendor/bin/phpunit --color=always diff --git a/composer/mikehaertl/php-pdftk/LICENSE b/composer/mikehaertl/php-pdftk/LICENSE new file mode 100644 index 0000000000..28dbb2a292 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Michael Härtl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/composer/mikehaertl/php-pdftk/README.md b/composer/mikehaertl/php-pdftk/README.md new file mode 100644 index 0000000000..ba99281d49 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/README.md @@ -0,0 +1,501 @@ +php-pdftk +========= + +[![GitHub Tests](https://github.com/mikehaertl/php-pdftk/workflows/Tests/badge.svg)](https://github.com/mikehaertl/php-pdftk/actions) +[![Packagist Version](https://img.shields.io/packagist/v/mikehaertl/php-pdftk?label=version)](https://packagist.org/packages/mikehaertl/php-pdftk) +[![Packagist Downloads](https://img.shields.io/packagist/dt/mikehaertl/php-pdftk)](https://packagist.org/packages/mikehaertl/php-pdftk) +[![GitHub license](https://img.shields.io/github/license/mikehaertl/php-pdftk)](https://github.com/mikehaertl/php-pdftk/blob/master/LICENSE) +[![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/mikehaertl/php-pdftk)](https://packagist.org/packages/mikehaertl/php-pdftk) + +A PDF conversion and form utility based on pdftk. + +## Features + +*php-pdftk* brings the full power of `pdftk` to PHP - and more. + + * Fill forms, either from a XFDF/FDF file or from a data array (UTF-8 safe for unflattened forms, requires pdftk 2.x !) + * Create XFDF or FDF files from PHP arrays (UTF-8 safe!) + * Create FDF files from filled PDF forms + * Combine pages from several PDF files into a new PDF file + * Split a PDF into one file per page + * Add background or overlay PDFs + * Read out meta data about PDF and form fields + * Set passwords and permissions + * Remove passwords + +## Requirements + + * The `pdftk` command must be installed and working on your system + * This library is written for pdftk 2.x versions. You should be able to use it with pdftk 1.x but not all methods will work there. + For details consult the man page of pdftk on your system. + * There is a [known issue](https://github.com/mikehaertl/php-pdftk/issues/150) + on Ubuntu if you installed the `pdftk` package from snap. This version has + no permission to write to the `/tmp` directory. You can either set another + temporay directory as described below or use another package. For Ubuntu + 18.10 there's also a `pdftk-java` package available via apt which should work + fine. You can also install this package on Ubuntu 18.04 if you download it + manually. Also check [this answer](https://askubuntu.com/a/1028983/175814) + on askubuntu. + +> **Note:** The pdftk version from the alternative PPA `ppa:malteworld/ppa` is +> no longer available. The author instead now points to his answer on askubuntu +> linked above. + +## Installation + +You should use [composer](https://getcomposer.org/) to install this library. + +``` +composer require mikehaertl/php-pdftk +``` + +## Examples + +### Create instance for PDF files + +There are several ways to tell the `Pdf` instance which file(s) it should use. +Some files may also require a password or need an alias to be used as a handle +in some operations (e.g. cat or shuffle). + +> **Note:** In version 2.x of pdftk a handle can be one or more upper case letters. + +```php +// Create an instance for a single file +$pdf = new Pdf('/path/to/form.pdf'); + +// Alternatively add files later. Handles are autogenerated in this case. +$pdf = new Pdf(); +$pdf->addFile('/path/to/file1.pdf'); +$pdf->addFile('/path/to/file2.pdf'); + +// Add files with own handle +$pdf = new Pdf(); +$pdf->addFile('/path/to/file1.pdf', 'A'); +$pdf->addFile('/path/to/file2.pdf', 'B'); +// Add file with handle and password +$pdf->addFile('/path/to/file3.pdf', 'C', 'secret*password'); + +// Shortcut to pass all files to the constructor +$pdf = new Pdf([ + 'A' => ['/path/to/file1.pdf', 'secret*password1'], + 'B' => ['/path/to/file2.pdf', 'secret*password2'], +]); +``` + +### Operations + +Please consult the `pdftk` man page for each operation to find out how each operation works +in detail and which options are available. + +For all operations you can either save the PDF locally through `saveAs($name)` or send it to the +browser with `send()`. If you pass a filename to `send($name)` the client browser will open a download +dialogue whereas without a filename it will usually display the PDF inline. + +**IMPORTANT: You can always only perform *one* of the following operations on a single PDF instance. +Below you can find a workaround if you need multiple operations.** + +#### Fill Form + +Fill a PDF form with data from a PHP array or an XFDF/FDF file. + +```php +use mikehaertl\pdftk\Pdf; + +// Fill form with data array +$pdf = new Pdf('/full/path/to/form.pdf'); +$result = $pdf->fillForm([ + 'name'=>'ÄÜÖ äüö мирано čárka', + 'nested.name' => 'valX', + ]) + ->needAppearances() + ->saveAs('filled.pdf'); + +// Always check for errors +if ($result === false) { + $error = $pdf->getError(); +} + +// Fill form from FDF +$pdf = new Pdf('form.pdf'); +$result = $pdf->fillForm('data.xfdf') + ->saveAs('filled.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +**Note:** When filling in UTF-8 data, you should always add the `needAppearances()` option. +This will make sure, that the PDF reader takes care of using the right fonts for rendering, +something that pdftk can't do for you. Also note that `flatten()` doesn't really work well +if you have special characters in your data. + +If you use `pdftk-java` >= 3.3.0 and the embedded font does not support UTF-8 +characters you can also replace it with a local font: + +```php +use mikehaertl\pdftk\Pdf; + +// Fill form with data array +$pdf = new Pdf('/full/path/to/form.pdf'); +$result = $pdf->fillForm($data) + ->replacementFont('/usr/share/fonts/dejavu/DejaVuSans.ttf') + ->saveAs('filled.pdf'); +``` + +#### Create a XFDF/FDF file from a PHP array + +This is a bonus feature that is not available from `pdftk`. + +```php +use mikehaertl\pdftk\XfdfFile; +use mikehaertl\pdftk\FdfFile; + +$xfdf = new XfdfFile(['name' => 'Jürgen мирано']); +$xfdf->saveAs('/path/to/data.xfdf'); + +$fdf = new FdfFile(['name' => 'Jürgen мирано']); +$fdf->saveAs('/path/to/data.fdf'); +``` + +#### Cat + +Assemble a PDF from pages from one or more PDF files. + +```php +use mikehaertl\pdftk\Pdf; + +// Extract pages 1-5 and 7,4,9 into a new file +$pdf = new Pdf('/path/to/my.pdf'); +$result = $pdf->cat(1, 5) + ->cat([7, 4, 9]) + ->saveAs('/path/to/new.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} + +// Combine pages from several files +$pdf = new Pdf([ + 'A' => '/path/file1.pdf', // A is alias for file1.pdf + 'B' => ['/path/file2.pdf','pass**word'], // B is alias for file2.pdf + 'C' => ['/path/file3.pdf','secret**pw'], // C is alias for file3.pdf +]); +$result = $pdf->cat(1, 5, 'A') // pages 1-5 from A + ->cat(3, null, 'B') // page 3 from B + ->cat(7, 'end', 'B', null, 'east') // pages 7-end from B, rotated East + ->cat('end',3,'A','even') // even pages 3-end in reverse order from A + ->cat([2,3,7], 'C') // pages 2,3 and 7 from C + ->saveAs('/path/new.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +#### Shuffle + +Like `cat()` but create "*streams*" and fill the new PDF with one page from each +stream at a time. + +```php +use mikehaertl\pdftk\Pdf; + +$pdf = new Pdf([ + 'A' => '/path/file1.pdf', // A is alias for file1.pdf + 'B' => '/path/file2.pdf', // B is alias for file2.pdf +]); + +// new.pdf will have pages A1, B3, A2, B4, A3, B5, ... +$result = $pdf->shuffle(1, 5, 'A') // pages 1-5 from A + ->shuffle(3, 8, 'B') // pages 3-8 from B + ->saveAs('/path/new.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +#### Burst + +Split a PDF file into one file per page. + +```php +use mikehaertl\pdftk\Pdf; + +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->burst('/path/page_%d.pdf'); // Supply a printf() pattern +if ($result === false) { + $error = $pdf->getError(); +} +``` + +#### Add background PDF + +Add another PDF file as background. + +```php +use mikehaertl\pdftk\Pdf; + +// Set background from another PDF (first page repeated) +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->background('/path/back.pdf') + ->saveAs('/path/watermarked.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} + +// Set background from another PDF (one page each) +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->multiBackground('/path/back_pages.pdf') + ->saveAs('/path/watermarked.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +#### Add overlay PDF + +Add another PDF file as overlay. + +```php +use mikehaertl\pdftk\Pdf; + +// Stamp with another PDF (first page repeated) +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->stamp('/path/overlay.pdf') + ->saveAs('/path/stamped.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} + +// Stamp with another PDF (one page each) +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->multiStamp('/path/overlay_pages.pdf') + ->saveAs('/path/stamped.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +#### Attach Files + +Add file attachments to the document or to a specific page. + +```php +use mikehaertl\pdftk\Pdf; + +$files = [ + '/path/to/file1', + '/path/to/file2', +] + +// Add files at the document level +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->attachFiles($files) + ->saveAs('/path/withfiles.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} + +// Add files to a specific page +$pdf = new Pdf('/path/my.pdf'); +$page = 7; +$result = $pdf->attachFiles($files, $page) + ->saveAs('/path/withfiles.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` +#### Unpack Files + +Copy file attachments from a PDF to the given directory. + +```php +use mikehaertl\pdftk\Pdf; + +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->unpackFiles('/path/to/dir'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +#### Generate FDF + +Create a FDF file from a given filled PDF form. + +```php +use mikehaertl\pdftk\Pdf; + +// Create FDF from PDF +$pdf = new Pdf('/path/form.pdf'); +$result = $pdf->generateFdfFile('/path/data.fdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +#### Get PDF data + +Read out metadata or form field information from a PDF file. + +```php +use mikehaertl\pdftk\Pdf; + +// Get data +$pdf = new Pdf('/path/my.pdf'); +$data = $pdf->getData(); +if ($data === false) { + $error = $pdf->getError(); +} + +// Get form data fields +$pdf = new Pdf('/path/my.pdf'); +$data = $pdf->getDataFields(); +if ($data === false) { + $error = $pdf->getError(); +} + +// Get data as string +echo $data; +$txt = (string) $data; +$txt = $data->__toString(); + +// Get data as array +$arr = (array) $data; +$arr = $data->__toArray(); +$field1 = $data[0]['Field1']; +``` + +#### How to perform more than one operation on a PDF + +As stated above, you can only perform one of the preceeding operations on a single PDF instance. +If you need more than one operation you can feed one `Pdf` instance into another: + +```php +use mikehaertl\pdftk\Pdf; + +// Extract pages 1-5 and 7,4,9 into a new file +$pdf = new Pdf('/path/my.pdf'); +$pdf->cat(1, 5) + ->cat([7, 4, 9]); + +// We now use the above PDF as source file for a new PDF +$pdf2 = new Pdf($pdf); +$result = $pdf2->fillForm(['name' => 'ÄÜÖ äüö мирано čárka']) + ->needAppearances() + ->saveAs('/path/filled.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +### Options + +You can combine the above operations with one or more of the following options. + +```php +use mikehaertl\pdftk\Pdf; + +$pdf = new Pdf('/path/my.pdf'); + +$result = $pdf->allow('AllFeatures') // Change permissions + ->flatten() // Merge form data into document (doesn't work well with UTF-8!) + ->compress($value) // Compress/Uncompress + ->keepId('first') // Keep first/last Id of combined files + ->dropXfa() // Drop newer XFA form from PDF + ->dropXmp() // Drop newer XMP data from PDF + ->needAppearances() // Make clients create appearance for form fields + ->setPassword($pw) // Set owner password + ->setUserPassword($pw) // Set user password + ->passwordEncryption(128) // Set password encryption strength + ->saveAs('new.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} + +// Example: Fill PDF form and merge form data into PDF +// Fill form with data array +$result = $pdf = new Pdf('/path/form.pdf'); +$pdf->fillForm(['name' => 'My Name']) + ->flatten() + ->saveAs('/path/filled.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} + +// Example: Remove password from a PDF +$pdf = new Pdf; +$result = $pdf->addFile('/path/my.pdf', null, 'some**password') + ->saveAs('/path/new.pdf'); +if ($result === false) { + $error = $pdf->getError(); +} +``` + +### Shell Command + +The class uses [php-shellcommand](https://github.com/mikehaertl/php-shellcommand) to execute +`pdftk`. You can pass `$options` for its `Command` class as second argument to the constructor: + +```php +use mikehaertl\pdftk\Pdf; + +$pdf = new Pdf('/path/my.pdf', [ + 'command' => '/some/other/path/to/pdftk', + // or on most Windows systems: + // 'command' => 'C:\Program Files (x86)\PDFtk\bin\pdftk.exe', + 'useExec' => true, // May help on Windows systems if execution fails +]); +``` + +#### Solve issues with UTF-8 characters in filenames or infofile content + +If you have files with UTF-8 encoded characters in their filename or if you +pass an infofile with such characters to `updateInfo()` you should supply the +correct locale when excuting `pdftk`. You can therefore add these options: + +```php +$pdf = new Pdf($file, [ + 'locale' => 'en_US.utf8', + 'procEnv' => [ + 'LANG' => 'en_US.utf-8', + ], +]); +``` + +> **Note:** You need to ensure that the locale you set here is available on +> your system. On Linux you can check with `locale -a` which locales are +> installed. [This article](https://wiki.archlinux.org/title/locale) explains +> the concept in more detail. + + + +### Temporary File + +Internally a temporary file is created via [php-tmpfile](https://github.com/mikehaertl/php-tmpfile). +You can also access that file directly, e.g. if you neither want to send or save the +file but only need the binary PDF content: + +```php +use mikehaertl\pdftk\Pdf; + +$pdf = new Pdf('/path/my.pdf'); +$result = $pdf->fillForm(['name' => 'My Name']) + ->execute(); +if ($result === false) { + $error = $pdf->getError(); +} +$content = file_get_contents( (string) $pdf->getTmpFile() ); +``` + +If you have permission issues you may have to set a directory where your +`pdftk` command can write to: + +```php +use mikehaertl\pdftk\Pdf; + +$pdf = new Pdf('/path/my.pdf'); +$pdf->tempDir = '/home/john/temp'; +``` + +## API + +Please consult the source files for a full documentation of each method. diff --git a/composer/mikehaertl/php-pdftk/composer.json b/composer/mikehaertl/php-pdftk/composer.json new file mode 100644 index 0000000000..155ba17279 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/composer.json @@ -0,0 +1,31 @@ +{ + "name": "mikehaertl/php-pdftk", + "description": "A PDF conversion and form utility based on pdftk.", + "keywords": ["pdf", "pdftk"], + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Michael Haertl", + "email": "haertl.mike@gmail.com" + } + ], + "require": { + "php": ">=5.3.0", + "mikehaertl/php-shellcommand": "^1.6.3", + "mikehaertl/php-tmpfile": "^1.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0 <9.4" + }, + "autoload": { + "psr-4": { + "mikehaertl\\pdftk\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "tests\\": "tests" + } + } +} diff --git a/composer/mikehaertl/php-pdftk/src/Command.php b/composer/mikehaertl/php-pdftk/src/Command.php new file mode 100644 index 0000000000..0919e6b886 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/src/Command.php @@ -0,0 +1,249 @@ + + * @license http://www.opensource.org/licenses/MIT + */ +class Command extends BaseCommand +{ + /** + * @var string the pdftk binary + */ + protected $_command = 'pdftk'; + + /** + * @var array list of input files to process as array('name' => $filename, + * 'password' => $pw) indexed by handle + */ + protected $_files = array(); + + /** + * @var array list of command options, either strings or array with + * arguments to addArg() + */ + protected $_options = array(); + + /** + * @var string the operation to perform + */ + protected $_operation; + + /** + * @var string|array operation arguments, e.g. a list of page ranges or a + * filename or tmp file instance + */ + protected $_operationArgument = array(); + + /** + * @var bool whether to force escaping of the operation argument e.g. for + * filenames + */ + protected $_escapeOperationArgument = false; + + /** + * @param string $name the PDF file to add for processing + * @param string $handle one or more uppercase letters A..Z to reference + * this file later. + * @param string|null $password the owner (or user) password if any + * @return Command the command instance for method chaining + * @throws \Exception + */ + public function addFile($name, $handle, $password = null) + { + $this->checkExecutionStatus(); + $file = array( + 'name' => $name, + 'password' => $password, + ); + $this->_files[$handle] = $file; + return $this; + } + + /** + * @param string $option the pdftk option to add + * @param string|File|null $argument the argument to add, either string, + * File instance or null if none + * @param null|bool whether to escape the option. Default is null meaning + * use Command default setting. + * @return Command the command instance for method chaining + */ + public function addOption($option, $argument = null, $escape = null) + { + $this->_options[] = $argument === null ? $option : array($option, $argument, $escape); + return $this; + } + + /** + * @param string $operation the operation to perform + * @return Command the command instance for method chaining + */ + public function setOperation($operation) + { + $this->checkExecutionStatus(); + $this->_operation = $operation; + return $this; + } + + /** + * @return string|null the current operation or null if none set + */ + public function getOperation() + { + return $this->_operation; + } + + /** + * @param string $value the operation argument + * @param bool $escape whether to escape the operation argument + * @return Command the command instance for method chaining + */ + public function setOperationArgument($value, $escape = false) + { + $this->checkExecutionStatus(); + $this->_operationArgument = $value; + $this->_escapeOperationArgument = $escape; + return $this; + } + + /** + * @return string|array|null the current operation argument as string or + * array or null if none set + */ + public function getOperationArgument() + { + // Typecast to string in case we have a File instance as argument + return is_array($this->_operationArgument) ? $this->_operationArgument : (string) $this->_operationArgument; + } + + /** + * @return int the number of files added to the command + */ + public function getFileCount() + { + return count($this->_files); + } + + /** + * Add a page range as used by some operations + * + * @param int|string|array $start the start page number or an array of page + * numbers. If an array, the other arguments will be ignored. $start can + * also be bigger than $end for pages in reverse order. + * @param int|string|null $end the end page number or null for single page + * (or list if $start is an array) + * @param string|null $handle the handle of the file to use. Can be null if + * only a single file was added. + * @param string|null $qualifier the page number qualifier, either 'even' + * or 'odd' or null for none + * @param string $rotation the rotation to apply to the pages. + * @return Command the command instance for method chaining + */ + public function addPageRange($start, $end = null, $handle = null, $qualifier = null, $rotation = null) + { + $this->checkExecutionStatus(); + if (is_array($start)) { + if ($handle !== null) { + $start = array_map(function ($p) use ($handle) { + return $handle . $p; + }, $start); + } + $range = implode(' ', $start); + } else { + $range = $handle . $start; + if ($end) { + $range .= '-' . $end; + } + $range .= $qualifier . $rotation; + } + $this->_operationArgument[] = $range; + return $this; + } + + /** + * @param string|null $filename the filename to add as 'output' option or + * null if none + * @return bool whether the command was executed successfully + */ + public function execute($filename = null) + { + $this->checkExecutionStatus(); + $this->processInputFiles(); + $this->processOperation(); + $this->processOptions($filename); + return parent::execute(); + } + + /** + * Process input PDF files and create respective command arguments + */ + protected function processInputFiles() + { + $passwords = array(); + foreach ($this->_files as $handle => $file) { + $this->addArg($handle . '=', $file['name']); + if ($file['password'] !== null) { + $passwords[$handle] = $file['password']; + } + } + if ($passwords !== array()) { + $this->addArg('input_pw'); + foreach ($passwords as $handle => $password) { + $this->addArg($handle . '=', $password); + } + } + } + + /** + * Process options and create respective command arguments + * @param string|null $filename if provided an 'output' option will be + * added + */ + protected function processOptions($filename = null) + { + // output must be first option after operation + if ($filename !== null) { + $this->addArg('output', $filename, true); + } + foreach ($this->_options as $option) { + if (is_array($option)) { + $this->addArg($option[0], $option[1], $option[2]); + } else { + $this->addArg($option); + } + } + } + + /** + * Process opearation and create respective command arguments + */ + protected function processOperation() + { + if ($this->_operation !== null) { + $value = $this->_operationArgument ? $this->_operationArgument : null; + if ($value instanceof TmpFile) { + $value = (string) $value; + } + $this->addArg($this->_operation, $value, $this->_escapeOperationArgument); + } + } + + /** + * Ensure that the command was not exectued yet. Throws exception + * otherwise. + * @throws \Exception + */ + protected function checkExecutionStatus() + { + if ($this->getExecuted()) { + throw new \Exception('Operation was already executed'); + } + } +} diff --git a/composer/mikehaertl/php-pdftk/src/DataFields.php b/composer/mikehaertl/php-pdftk/src/DataFields.php new file mode 100644 index 0000000000..a5b40f74e0 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/src/DataFields.php @@ -0,0 +1,166 @@ + + * @author Michael Härtl + * @license http://www.opensource.org/licenses/MIT + */ +class DataFields extends ArrayObject +{ + private $_string; + private $_array; + + /** + * DataFields constructor. + * + * @param string $input + * @param int $flags + * @param string $iterator_class + */ + public function __construct($input = null, $flags = 0, $iterator_class = "ArrayIterator") + { + $this->_string = $input ?: ''; + $this->_array = self::parse($this->_string); + + return parent::__construct($this->_array, $flags, $iterator_class); + } + + /** + * @return string + */ + public function __toString() + { + return $this->_string; + } + + /** + * @return array + */ + public function __toArray() + { + return $this->_array; + } + + /** + * Parse the output of dump_data_fields into an array. + * + * The string to parse can either be a single block of `Xyz:value` lines + * or a set of such blocks, separated by and starting with `---`. + * + * + * Here's an example: + * + * ``` + * --- + * FieldType: Text + * FieldName: Text1 + * FieldFlags: 0 + * FieldValue: University of Missouri : Ray-Holland + * extended line value + * FieldValueDefault: University of Missouri : Ray-Holland + * extended line2 value + * FieldJustification: Left + * FieldMaxLength: 99 + * --- + * FieldType: Text + * FieldName: Text2 + * ... + * ... + * ``` + * + * @param $input the string to parse + * @return array the parsed result + */ + public static function parse($input) + { + if (strncmp('---', $input, 3) === 0) { + // Split blocks only if '---' is followed by 'FieldType' + $blocks = preg_split( + '/^---(\r\n|\n|\r)(?=FieldType:)/m', + substr($input, 3) + ); + return array_map('\mikehaertl\pdftk\DataFields::parseBlock', $blocks); + } else { + return self::parseBlock($input); + } + } + + /** + * Parses a block of this form: + * + * ``` + * Name1: Value1 + * Name2: Value2 + * Name3: Value3 + * ... + * ``` + * + * @param string $block the block to parse + * @return array the parsed block values indexed by respective names + */ + public static function parseBlock($block) + { + $data = array(); + $lines = preg_split("/(\r\n|\n|\r)/", trim($block)); + $continueKey = null; + foreach ($lines as $n => $line) { + if ($continueKey !== null) { + $data[$continueKey] .= "\n" . $line; + if (!self::lineContinues($lines, $n, $continueKey)) { + $continueKey = null; + } + } elseif (preg_match('/([^:]*): ?(.*)/', $line, $match)) { + $key = $match[1]; + $value = $match[2]; + // Convert multiple keys like 'FieldStateOption' or 'FieldValue' + // from Choice fields to array + if (isset($data[$key])) { + $data[$key] = (array) $data[$key]; + $data[$key][] = $value; + } else { + $data[$key] = $value; + } + if (self::lineContinues($lines, $n, $key)) { + $continueKey = $key; + } + } + } + return $data; + } + + /** + * Checks whether the value for the given line number continues on the next + * line, i.e. is a multiline string. + * + * This can be the case for 'FieldValue' and 'FieldValueDefault' keys. To + * find the end of the string we don't simply test for /^Field/, as this + * would also match multiline strings where a line starts with 'Field'. + * + * Instead we assume that the string is always followed by one of these + * keys: + * + * - 'FieldValue:' + * - 'FieldValueDefault:' + * - 'FieldJustification:' + * + * @param array $lines all lines of the block + * @param int $n the 0-based index of the current line + * @param string the key for the value. Only 'FieldValue' and + * 'FieldValueDefault' can span multiple lines + * @return bool whether the value continues in line n + 1 + */ + protected static function lineContinues($lines, $n, $key) + { + return + in_array($key, array('FieldValue', 'FieldValueDefault')) && + array_key_exists($n + 1, $lines) && + !preg_match('/^Field(Value|ValueDefault|Justification):/', $lines[$n + 1]); + } +} diff --git a/composer/mikehaertl/php-pdftk/src/FdfFile.php b/composer/mikehaertl/php-pdftk/src/FdfFile.php new file mode 100644 index 0000000000..269ec0a8e1 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/src/FdfFile.php @@ -0,0 +1,84 @@ + + * @license http://www.opensource.org/licenses/MIT + */ +class FdfFile extends File +{ + // FDF file header + const FDF_HEADER = <<> >> +endobj +trailer +<> +%%EOF +FDF; + + /** + * Constructor + * + * @param array $data the form data as name => value + * @param string|null $suffix the optional suffix for the tmp file + * @param string|null $suffix the optional prefix for the tmp file. If null + * 'php_tmpfile_' is used. + * @param string|null $directory directory where the file should be + * created. Autodetected if not provided. + * @param string|null $encoding of the data. Default is 'UTF-8'. + */ + public function __construct($data, $suffix = null, $prefix = null, $directory = null, $encoding = 'UTF-8') + { + if ($directory === null) { + $directory = self::getTempDir(); + } + $suffix = '.fdf'; + $prefix = 'php_pdftk_fdf_'; + + $this->_fileName = tempnam($directory, $prefix); + $newName = $this->_fileName . $suffix; + rename($this->_fileName, $newName); + $this->_fileName = $newName; + + if (!function_exists('mb_convert_encoding')) { + throw new \Exception('MB extension required.'); + } + + $fields = ''; + foreach ($data as $key => $value) { + // Create UTF-16BE string encode as ASCII hex + // See http://blog.tremily.us/posts/PDF_forms/ + $utf16Value = mb_convert_encoding($value, 'UTF-16BE', $encoding); + + /* Also create UTF-16BE encoded key, this allows field names containing + * german umlauts and most likely many other "special" characters. + * See issue #17 (https://github.com/mikehaertl/php-pdftk/issues/17) + */ + $utf16Key = mb_convert_encoding($key, 'UTF-16BE', $encoding); + + // Escape parenthesis + $utf16Value = strtr($utf16Value, array('(' => '\\(', ')' => '\\)')); + $fields .= "<>\n"; + } + + // Use fwrite, since file_put_contents() messes around with character encoding + $fp = fopen($this->_fileName, 'w'); + fwrite($fp, self::FDF_HEADER); + fwrite($fp, $fields); + fwrite($fp, self::FDF_FOOTER); + fclose($fp); + } +} diff --git a/composer/mikehaertl/php-pdftk/src/InfoFields.php b/composer/mikehaertl/php-pdftk/src/InfoFields.php new file mode 100644 index 0000000000..09f46556ee --- /dev/null +++ b/composer/mikehaertl/php-pdftk/src/InfoFields.php @@ -0,0 +1,139 @@ + + * @license http://www.opensource.org/licenses/MIT + */ +class InfoFields extends ArrayObject +{ + private $_string; + + private $_array; + + /** + * InfoFields constructor. + * + * @param string $input + * @param int $flags + * @param string $iterator_class + */ + public function __construct($input = null, $flags = 0, $iterator_class = "ArrayIterator") + { + $this->_string = $input ?: ''; + $this->_array = $this->parseData($this->_string); + + return parent::__construct($this->_array, $flags, $iterator_class); + } + + /** + * @return string + */ + public function __toString() + { + return $this->_string; + } + + /** + * @return array + */ + public function __toArray() + { + return $this->_array; + } + + /** + * Parse the output of dump_data into something usable. + * + * The expected string looks similar to this: + * + * InfoBegin + * InfoKey: Creator + * InfoValue: Adobe Acrobat Pro DC 15.0 + * InfoBegin + * InfoKey: Producer + * InfoValue: XYZ + * PdfID0: 1fdce9ed1153ab4c973334b512a67997 + * PdfID1: c7acc878cda02ad7bb401fa8080a8929 + * NumberOfPages: 11 + * BookmarkBegin + * BookmarkTitle: First bookmark + * BookmarkLevel: 1 + * BookmarkPageNumber: 1 + * BookmarkBegin + * BookmarkTitle: Second bookmark + * BookmarkLevel: 1 + * BookmarkPageNumber: 2 + * + * @param $dataString + * @return array + */ + private function parseData($dataString) + { + $output = array(); + foreach (explode(PHP_EOL, $dataString) as $line) { + $trimmedLine = trim($line); + // Parse blocks of the form: + // AbcBegin + // AbcData1: Value1 + // AbcData2: Value2 + // AbcBegin + // AbcData1: Value3 + // AbcData2: Value4 + // ... + if (preg_match('/^(\w+)Begin$/', $trimmedLine, $matches)) { + // Previous group ended - if any - so add it to output + if (!empty($group) && !empty($groupData)) { + $output[$group][] = $groupData; + } + // Now start next group + $group = $matches[1]; // Info, PageMedia, ... + if (!isset($output[$group])) { + $output[$group] = array(); + } + $groupData = array(); + continue; + } + if (!empty($group)) { + // Check for AbcData1: Value1 + if (preg_match("/^$group(\w+): ?(.*)$/", $trimmedLine, $matches)) { + $groupData[$matches[1]] = $matches[2]; + continue; + } else { + // Something else, so group ended + if (!empty($groupData)) { + $output[$group][] = $groupData; + $groupData = array(); + } + $group = null; + } + } + if (preg_match('/([^:]*): ?(.*)/', $trimmedLine, $matches)) { + $output[$matches[1]] = $matches[2]; + } + } + // There could be a final group left if it was not followed by another + // line in the loop + if (!empty($group) && !empty($groupData)) { + $output[$group][] = $groupData; + } + + // Info group is a list of ['Key' => 'x', 'Value' => 'y'], so + // convert it to ['x' => 'y', ...] + if (isset($output['Info'])) { + $data = array(); + foreach ($output['Info'] as $infoGroup) { + if (isset($infoGroup['Key'], $infoGroup['Value'])) { + $data[$infoGroup['Key']] = $infoGroup['Value']; + } + } + $output['Info'] = $data; + } + return $output; + } +} diff --git a/composer/mikehaertl/php-pdftk/src/InfoFile.php b/composer/mikehaertl/php-pdftk/src/InfoFile.php new file mode 100644 index 0000000000..0325dfa817 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/src/InfoFile.php @@ -0,0 +1,190 @@ + + * @license http://www.opensource.org/licenses/MIT + */ +class InfoFile extends File +{ + /** + * @var string[] list of valid keys for the document information directory of + * the PDF. These will be converted into `InfoBegin... InfoKey... InvoValue` + * blocks on the output. + * + * See section 14.3.3 in https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf + */ + public static $documentInfoFields = array( + 'Title', + 'Author', + 'Subject', + 'Keywords', + 'Creator', + 'Producer', + 'CreationDate', + 'ModDate', + 'Trapped', + ); + + /** + * Constructor + * + * @param array|InfoFields $data the data in this format: + * ``` + * [ + * 'Info' => [ + * 'Title' => '...', + * 'Author' => '...', + * 'Subject' => '...', + * 'Keywords' => '...', + * 'Creator' => '...', + * 'Producer' => '...', + * 'CreationDate' => '...', + * 'ModDate' => '...', + * 'Trapped' => '...', + * ], + * 'Bookmark' => [ + * [ + * 'Title' => '...', + * 'Level' => ..., + * 'PageNumber' => ..., + * ], + * ], + * 'PageMedia' => [ ... ], + * 'PageLabel' => [ ... ], + * // ... + * ] + * ``` + * This is the same format as the InfoFields object that is returned + * by `getData()` if you cast it to an array. You can also pass such an + * (optionally modified) object as input. Some fields like 'NumberOfPages' + * or 'PdfID0' are ignored as those are not part of the PDF's metadata. + * All array elements are optional. + * @param string|null $suffix the optional suffix for the tmp file + * @param string|null $suffix the optional prefix for the tmp file. If null + * 'php_tmpfile_' is used. + * @param string|null $directory directory where the file should be + * created. Autodetected if not provided. + * @param string|null $encoding of the data. Default is 'UTF-8'. If the + * data has another encoding it will be converted to UTF-8. This requires + * the mbstring extension to be installed. + * @throws Exception on invalid data format or if mbstring extension is + * missing and data must be converted + */ + public function __construct($data, $suffix = null, $prefix = null, $directory = null, $encoding = 'UTF-8') + { + if ($suffix === null) { + $suffix = '.txt'; + } + if ($prefix === null) { + $prefix = 'php_pdftk_info_'; + } + if ($directory === null) { + $directory = self::getTempDir(); + } + + $tempName = tempnam($directory, $prefix); + $newName = $tempName . $suffix; + rename($tempName, $newName); + $this->_fileName = $newName; + + if ($encoding !== 'UTF-8' && !function_exists('mb_convert_encoding')) { + throw new Exception('mbstring extension required.'); + } + + $fields = ''; + $normalizedData = self::normalize($data); + + foreach ($normalizedData as $block => $items) { + $fields .= self::renderBlock($block, $items, $encoding); + } + + // Use fwrite, since file_put_contents() messes around with character encoding + $fp = fopen($this->_fileName, 'w'); + fwrite($fp, $fields); + fclose($fp); + } + + /** + * Normalize the input data + * + * This also converts data from the legacy format (<0.13.0) to the new + * input format described in the constructor. + * + * @param array $data the data to normalize + * @return array a normalized array in the format described in the constructor + */ + private static function normalize($data) + { + $normalized = array(); + foreach ($data as $key => $value) { + if (in_array($key, self::$documentInfoFields)) { + $normalized['Info'][$key] = $value; + } elseif (is_array($value)) { + if (!isset($normalized[$key])) { + $normalized[$key] = array(); + } + $normalized[$key] = array_merge($normalized[$key], $value); + } + } + return $normalized; + } + + /** + * Render a set of block fields + * + * @param string $block like 'Info', 'Bookmark', etc. + * @param array $items the field items to render + * @param string $encoding the encoding of the item data + * @return string the rendered fields + */ + private static function renderBlock($block, $items, $encoding) + { + $fields = ''; + foreach ($items as $key => $value) { + if ($block === 'Info') { + $fields .= self::renderField($block, $key, $value, $encoding, true); + } else { + $fields .= "{$block}Begin\n"; + foreach ($value as $subKey => $subValue) { + $fields .= self::renderField($block, $subKey, $subValue, $encoding, false); + } + } + } + return $fields; + } + + /** + * Render a field in a given input block + * + * @param string $prefix the prefix to use for the field + * @param string $key the field key + * @param string $value the field value + * @param string $encoding the endoding of key and value + * @param bool $isInfo whether it's an 'Info' field + * @return string the rendered field + */ + private static function renderField($prefix, $key, $value, $encoding, $isInfo) + { + if ($encoding !== 'UTF-8') { + $value = mb_convert_encoding($value, 'UTF-8', $encoding); + $key = mb_convert_encoding($key, 'UTF-8', $encoding); + $value = defined('ENT_XML1') ? htmlspecialchars($key, ENT_XML1, 'UTF-8') : htmlspecialchars($key); + $key = defined('ENT_XML1') ? htmlspecialchars($value, ENT_XML1, 'UTF-8') : htmlspecialchars($value); + } + if ($isInfo) { + return "InfoBegin\nInfoKey: $key\nInfoValue: $value\n"; + } else { + return "{$prefix}{$key}: $value\n"; + } + + } +} diff --git a/composer/mikehaertl/php-pdftk/src/Pdf.php b/composer/mikehaertl/php-pdftk/src/Pdf.php new file mode 100644 index 0000000000..f50ed8a6b7 --- /dev/null +++ b/composer/mikehaertl/php-pdftk/src/Pdf.php @@ -0,0 +1,749 @@ + + * @license http://www.opensource.org/licenses/MIT + */ +class Pdf +{ + // The prefix for temporary files + const TMP_PREFIX = 'tmp_php_pdftk_'; + + /** + * @var bool whether to ignore any errors if some non-empty output file was + * still created. Default is false. + */ + public $ignoreWarnings = false; + + /** + * @var null|string an optional directory where temporary files should be + * created. If left empty the directory is autodetected. + */ + public $tempDir; + + /** + * @var File the temporary output file + */ + protected $_tmpFile; + + /** + * @var string the content type of the tmp output + */ + protected $_tmpOutputContentType = 'application/pdf'; + + /** + * @var Command the command instance that executes pdftk + */ + protected $_command; + + /** + * @var int a counter for autogenerated handles + */ + protected $_handle = 0; + + /** + * @var string the error message + */ + protected $_error = ''; + + /** + * @var string|null the output filename. If null (default) a tmp file is + * used as output. If false, no output option is added at all. + */ + protected $_output; + + /** + * @var string the PDF data as returned from getData() + */ + protected $_data; + protected $_data_utf8; + + /** + * @var DataFields the PDF form field data as returned from getDataFields() + */ + protected $_dataFields; + protected $_dataFields_utf8; + + /** + * @var Pdf[]|null if the input was an instance, we keep a reference here, + * so that it won't get unlinked before this object gets destroyed + */ + protected $_pdfs; + + /** + * @param string|Pdf|array $pdf a pdf filename or Pdf instance or an array + * of filenames/instances indexed by a handle. The array values can also + * be arrays of the form array($filename, $password) if some files are + * password protected. + * @param array $options Options to pass to set on the Command instance, + * e.g. the pdftk binary path + */ + public function __construct($pdf = null, $options = array()) + { + $command = $this->getCommand(); + if ($options !== array()) { + $command->setOptions($options); + } + if (is_string($pdf) || $pdf instanceof Pdf) { + $this->addFile($pdf); + } elseif (is_array($pdf)) { + foreach ($pdf as $handle => $file) { + if (is_array($file)) { + $this->addFile($file[0], $handle, $file[1]); + } else { + $this->addFile($file, $handle); + } + } + } + } + + /** + * @param string|Pdf $name the PDF filename or Pdf instance to add for + * processing + * @param string|null $handle one or more uppercase letters A..Z to + * reference this file later. If no handle is provided, an internal handle + * is autocreated, consuming the range Z..A + * @param string|null $password the owner (or user) password if any + * @return Pdf the pdf instance for method chaining + */ + public function addFile($name, $handle = null, $password = null) + { + if ($handle === null || is_numeric($handle)) { + $handle = $this->nextHandle(); + } + if ($name instanceof Pdf) { + // Keep a reference to the object to prevent unlinking + $this->_pdfs[] = $name; + if (!$name->getCommand()->getExecuted()) { + // @todo: Catch errors! + $name->execute(); + } + $name = (string) $name->getTmpFile(); + } + $this->getCommand()->addFile($name, $handle, $password); + return $this; + } + + /** + * Assemble (catenate) pages from the input files. + * + * Values for rotation are (in degrees): north: 0, east: 90, south: 180, + * west: 270, left: -90, right: +90, down: +180. left, right and down make + * relative adjustments to a page's rotation. Note: Older pdftk versions + * use N, E, S, W, L, R, and D instead. + * + * Example: + * + * $pdf = new Pdf; + * $pdf->addFile('file1.pdf', 'A') + * ->addFile('file2.pdf', 'B') + * ->cat(array(1,3),'B')) // pages 1 and 3 of file B + * ->cat(1, 5, 'A', 'odd') // pages 1, 3, 5 of file A + * ->cat('end', 5, 'B') // pages 5 to end of file B in reverse order + * ->cat(null, null, 'B', 'east') // All pages from file B rotated by 90 degree + * ->saveAs('out.pdf'); + * or + * $files = ['file1.pdf', 'file2.pdf', 'file3.pdf']; + * $pdf = new Pdf($files); + * $pdf->cat() // all files, all pages + * ->saveAs('out.pdf'); + * + * @param int|string|array|null $start the start page number or an array of page + * numbers. If an array, the other arguments will be ignored. $start can + * also be bigger than $end for pages in reverse order. If $start is null all + * pages of all files will be added. + * @param int|string|null $end the end page number or null for single page + * (or list if $start is an array) + * @param string|null $handle the handle of the file to use. Can be null if + * only a single file was added. + * @param string|null $qualifier the page number qualifier, either 'even' + * or 'odd' or null for none + * @param string $rotation the rotation to apply to the pages. + * @return Pdf the pdf instance for method chaining + */ + public function cat($start = null, $end = null, $handle = null, $qualifier = null, $rotation = null) + { + $this->getCommand() + ->setOperation('cat') + ->addPageRange($start, $end, $handle, $qualifier, $rotation); + return $this; + } + + /** + * Shuffle pages from the input files. + * + * This works the same as cat(), but each call to this method creates a + * "stream" of pages. The outfile will be assembled by adding one page from + * each stream at a time. + * + * Example: + * + * $pdf = new Pdf; + * $pdf1 = $pdf->addFile('file1.pdf'); + * $pdf->shuffle($pdf1, array(1,3,2)) + * ->shuffle($pdf1, array(4,5,9) + * ->saveAs('out.pdf'); + * + * This will give the page order 1, 4, 3, 5, 2, 9 in the out.pdf + * + * @param string $handle the handle of the input file to use + * @param int|array $start the start page number or an array of page + * numbers. + * @param int|null $end the end page number or null for single page (or + * list if $start is an array) + * @param string|null $qualifier the page number qualifier, either 'even' + * or 'odd' or null for none + * @param string $rotation the rotation to apply to the pages. See cat() + * for more details. + * @return Pdf the pdf instance for method chaining + */ + public function shuffle($start, $end = null, $handle = null, $qualifier = null, $rotation = null) + { + $this->getCommand() + ->setOperation('shuffle') + ->addPageRange($start, $end, $handle, $qualifier, $rotation); + return $this; + } + + /** + * Split the PDF document into pages + * + * @param string|null $filepattern the output name in sprintf format or + * null for default 'pg_%04d.pdf' + * @return bool whether the burst operation was successful + */ + public function burst($filepattern = null) + { + $this->constrainSingleFile(); + $this->getCommand()->setOperation('burst'); + $this->_output = $filepattern === null ? 'pg_%04d.pdf' : $filepattern; + return $this->execute(); + } + + /** + * Attach files to the PDF + * + * @param array $files the list of full paths to the files to attach + * @param string $toPage the page to add the attachment to. If omitted the + * files are attached at the document level. + * @return bool whether the operation was successful + */ + public function attachFiles($files, $toPage = null) + { + $this->constrainSingleFile(); + if ($toPage !== null) { + $files[] = 'to_page'; + $files[] = $toPage; + } + $this->getCommand() + ->setOperation('attach_files') + ->setOperationArgument($files, true); + return $this; + } + + /** + * Copy all attachments from the PDF to the given directory + * + * @param string|null $dir the output directory + * @return bool whether the operation was successful + */ + public function unpackFiles($dir = null) + { + $this->constrainSingleFile(); + $this->getCommand()->setOperation('unpack_files'); + $this->_output = $dir; + return $this->execute(); + } + + /** + * Generate the FDF file for a single PDF file. + * + * @param string $name name of the FDF file + * @return bool whether the pdf is generated successful + */ + public function generateFdfFile($name) + { + $this->constrainSingleFile(); + $this->getCommand()->setOperation('generate_fdf'); + $this->_output = $name; + return $this->execute(); + } + + /** + * Fill a PDF form + * + * @param string|array $data either a XFDF/FDF filename or an array with + * form field data (name => value) + * @param string $encoding the encoding of the data. Default is 'UTF-8'. + * @param bool $dropXfa whether to drop XFA forms (see dropXfa()). Default + * is true. + * @param string $format the file format to use for form filling when + * passing an array in `$data`. This can be `xfdf` or `fdf`. `xfdf` should + * give best results so you should not have to change the default. + * @return Pdf the pdf instance for method chaining + */ + public function fillForm($data, $encoding = 'UTF-8', $dropXfa = true, $format = 'xfdf') + { + $this->constrainSingleFile(); + if (is_array($data)) { + $className = '\mikehaertl\pdftk\\' . ($format === 'xfdf' ? 'XfdfFile' : 'FdfFile'); + $data = new $className($data, null, null, $this->tempDir, $encoding); + } + $this->getCommand() + ->setOperation('fill_form') + ->setOperationArgument($data, true); + + if ($dropXfa) { + $this->dropXfa(); + } + return $this; + } + + /** + * Update meta data of PDF + * + * @param string|array $data either a InfoFile filename or an array with + * form field data (name => value) + * @param string the encoding of the data. Default is 'UTF-8'. + * @return Pdf the pdf instance for method chaining + */ + public function updateInfo($data, $encoding = 'UTF-8') + { + $this->constrainSingleFile(); + if (is_array($data) || $data instanceof InfoFields) { + $data = new InfoFile($data, null, null, $this->tempDir, $encoding); + } + $this->getCommand() + ->setOperation($encoding == 'UTF-8' ? 'update_info_utf8' : 'update_info') + ->setOperationArgument($data, true); + + return $this; + } + + /** + * Apply a PDF as watermark to the background of a single PDF file. + * + * The PDF file must have a transparent background for the watermark to be + * visible. + * + * @param string $file name of the background PDF file. Only the first page + * is used. + * @return Pdf the pdf instance for method chaining + */ + public function background($file) + { + $this->constrainSingleFile(); + $this->getCommand() + ->setOperation('background') + ->setOperationArgument($file, true); + return $this; + } + + /** + * Apply multiple PDF pages as watermark to the corresponding pages of a + * single PDF file. + * + * If $file has fewer pages than the PDF file then the last page is + * repeated as background. + * + * @param string $file name of the background PDF file. + * @return Pdf the pdf instance for method chaining + */ + public function multiBackground($file) + { + $this->getCommand() + ->setOperation('multibackground') + ->setOperationArgument($file, true); + return $this; + } + + /** + * Add $file as overlay to a single PDF file. + * + * The $file should have a transparent background. + * + * @param string $file name of the PDF file to add as overlay. Only the + * first page is used. + * @return Pdf the pdf instance for method chaining + */ + public function stamp($file) + { + $this->constrainSingleFile(); + $this->getCommand() + ->setOperation('stamp') + ->setOperationArgument($file, true); + return $this; + } + + /** + * Add multiple pages from $file as overlay to the corresponding pages of a + * single PDF file. + * + * If $file has fewer pages than the PDF file then the last page is + * repeated as overlay. + * + * @param string $file name of the PDF file to add as overlay + * @return Pdf the pdf instance for method chaining + */ + public function multiStamp($file) + { + $this->getCommand() + ->setOperation('multistamp') + ->setOperationArgument($file, true); + return $this; + } + + /** + * @param bool $utf8 whether to dump the data UTF-8 encoded. Default is + * true. + * @return InfoFields|bool meta data about the PDF or false on failure + */ + public function getData($utf8 = true) + { + $property = $utf8 ? '_data_utf8' : '_data'; + if ($this->$property === null) { + $command = $this->getCommand(); + $command->setOperation($utf8 ? 'dump_data_utf8' : 'dump_data'); + if (!$command->execute()) { + $this->_error = $command->getError(); + return false; + } else { + $this->$property = new InfoFields(trim($command->getOutput())); + } + } + return $this->$property; + } + + /** + * @param bool $utf8 whether to dump the data UTF-8 encoded. Default is + * true. + * @return DataFields|bool data about the PDF form fields or false on + * failure + */ + public function getDataFields($utf8 = true) + { + $property = $utf8 ? '_dataFields_utf8' : '_dataFields'; + if ($this->$property === null) { + $command = $this->getCommand(); + $command->setOperation($utf8 ? 'dump_data_fields_utf8' : 'dump_data_fields'); + if (!$command->execute()) { + $this->_error = $command->getError(); + return false; + } else { + $this->$property = new DataFields(trim($command->getOutput())); + } + } + return $this->$property; + } + + /** + * Set PDF permissions + * + * + * @param string|null $permissions list of space separated permissions or + * null for none. The available permissions are Printing, DegradedPrinting, + * ModifyContents, Assembly, CopyContents, ScreenReaders, + * ModifyAnnotations, FillIn, AllFeatures. + * @return Pdf the pdf instance for method chaining + */ + public function allow($permissions = null) + { + $this->getCommand() + ->addOption('allow', $permissions, false); + return $this; + } + + /** + * Flatten the PDF form fields values into a single PDF file. + * + * @return Pdf the pdf instance for method chaining + */ + public function flatten() + { + $this->getCommand() + ->addOption('flatten'); + return $this; + } + + /** + * Restore/remove compression + * + * @param bool $compress whether to restore (default) or remove the + * compression + * @return Pdf the pdf instance for method chaining + */ + public function compress($compress = true) + { + $this->getCommand() + ->addOption($compress ? 'compress' : 'uncompress'); + return $this; + } + + /** + * When combining multiple PDFs, use either the first or last ID in the + * output. If not called, a new ID is created. + * + * @param string $id, either 'first' (default) or 'last' + * @return Pdf the pdf instance for method chaining + */ + public function keepId($id = 'first') + { + $this->getCommand() + ->addOption($id === 'first' ? 'keep_first_id' : 'keep_final_id'); + return $this; + } + + /** + * Set need_appearances flag in PDF + * + * This flag makes sure, that a PDF reader takes care of rendering form + * field content, even if it contains non ASCII characters. You should + * always use this option if you fill in forms e.g. with Unicode + * characters. You can't combine this option with flatten() though! + * + * @return Pdf the pdf instance for method chaining + */ + public function needAppearances() + { + $this->getCommand() + ->addOption('need_appearances'); + return $this; + } + + /** + * Drop XFA data from forms created with newer Acrobat. + * + * Newer PDF forms contain both, the newer XFA and the older AcroForm form + * fields. PDF readers can use both, but will prefer XFA if present. Since + * pdftk can only fill in AcroForm data you should always add this option + * when filling in forms with pdftk. + * + * @return Pdf the pdf instance for method chaining + */ + public function dropXfa() + { + $this->getCommand() + ->addOption('drop_xfa'); + return $this; + } + + /** + * Drop XMP meta data + * + * Newer PDFs can contain both, new style XMP data and old style info + * directory. PDF readers can use both, but will prefer XMP if present. + * Since pdftk can only update the info directory you should always add + * this option when updating PDF info. + * + * @return Pdf the pdf instance for method chaining + */ + public function dropXmp() + { + $this->getCommand() + ->addOption('drop_xmp'); + return $this; + } + + /** + * @param string $password the owner password to set on the output PDF + * @return Pdf the pdf instance for method chaining + */ + public function setPassword($password) + { + $this->getCommand() + ->addOption('owner_pw', $password, true); + return $this; + } + + /** + * @param string $password the user password to set on the output PDF + * @return Pdf the pdf instance for method chaining + */ + public function setUserPassword($password) + { + $this->getCommand() + ->addOption('user_pw', $password, true); + return $this; + } + + /** + * @param int $strength the password encryption strength. Default is 128 + * @return Pdf the pdf instance for method chaining + */ + public function passwordEncryption($strength = 128) + { + $this->getCommand() + ->addOption($strength == 128 ? 'encrypt_128bit' : 'encrypt_40bit'); + return $this; + } + + /** + * Replace embedded font with a local font when filling a form. + * + * This option is only available for pdftk-java >= 3.3.0. It is useful when + * filling a form with non-ASCII text that is not supported by the fonts + * included in the input PDF. + * + * @param string $fontName the path to the font or the name of a font family. + * @return Pdf the pdf instance for method chaining + */ + public function replacementFont($path) + { + $this->getCommand() + ->addOption('replacement_font', $path); + return $this; + } + + /** + * Execute the operation and save the output file + * + * @param string $name of output file + * @return bool whether the PDF could be processed and saved + */ + public function saveAs($name) + { + if (!$this->getCommand()->getExecuted() && !$this->execute()) { + return false; + } + $tmpFile = (string) $this->getTmpFile(); + if (!copy($tmpFile, $name)) { + $this->_error = "Could not copy PDF from tmp location '$tmpFile' to '$name'"; + return false; + } + return true; + } + + /** + * Send PDF to client, either inline or as download (triggers PDF creation) + * + * @param string|null $filename the filename to send. If empty, the PDF is + * streamed inline. + * @param bool $inline whether to force inline display of the PDF, even if + * filename is present. + * @param array $headers a list of additional HTTP headers to send in the + * response as an array. The array keys are the header names like + * 'Cache-Control' and the array values the header value strings to send. + * Each array value can also be another array of strings if the same header + * should be sent multiple times. This can also be used to override + * automatically created headers like 'Expires' or 'Content-Length'. To suppress + * automatically created headers, `false` can also be used as header value. + * @return bool whether PDF was created successfully + */ + public function send($filename = null, $inline = false, $headers = array()) + { + if (!$this->getCommand()->getExecuted() && !$this->execute()) { + return false; + } + $this->getTmpFile()->send($filename, $this->_tmpOutputContentType, $inline, $headers); + return true; + } + + /** + * Get the raw PDF contents (triggers PDF creation). + * + * @return string|bool the PDF content as a string or `false` if the PDF + * wasn't created successfully. + */ + public function toString() + { + if (!$this->getCommand()->getExecuted() && !$this->execute()) { + return false; + } + return file_get_contents($this->getTmpFile()->getFileName()); + } + + /** + * @return Command the command instance that executes pdftk + */ + public function getCommand() + { + if ($this->_command === null) { + $this->_command = new Command; + } + return $this->_command; + } + + /** + * @return File the temporary output file instance + */ + public function getTmpFile() + { + if ($this->_tmpFile === null) { + $this->_tmpFile = new File('', '.pdf', self::TMP_PREFIX, $this->tempDir); + } + return $this->_tmpFile; + } + + /** + * @return string the error message or an empty string if none + */ + public function getError() + { + return $this->_error; + } + + /** + * Execute the pdftk command and store the output file to a temporary + * location or $this->_output if set. You should probably never call this + * method unless you only need a temporary PDF file as result. + * + * @return bool whether the command was executed successfully + */ + public function execute() + { + $command = $this->getCommand(); + if ($command->getExecuted()) { + return false; + } + + if ($this->_output === false) { + $filename = null; + } else { + $filename = $this->_output ? $this->_output : (string) $this->getTmpFile(); + } + if (!$command->execute($filename)) { + $this->_error = $command->getError(); + if ($filename && !(file_exists($filename) && filesize($filename) !== 0 && $this->ignoreWarnings)) { + return false; + } + } + return true; + } + + /** + * Make sure, that only one file is present + */ + protected function constrainSingleFile() + { + if ($this->getCommand()->getFileCount() > 1) { + throw new \Exception('This operation can only process single files'); + } + } + + /** + * @return string the next handle in the series A, B, C, ... Z, AA, AB... + */ + protected function nextHandle() + { + // N.B. Multi-character handles are only available in pdftk 1.45+ + + $i = $this->_handle++; + $char = 'A'; + while ($i-- > 0) { + $char++; + } + + return $char; + } +} diff --git a/composer/mikehaertl/php-pdftk/src/XfdfFile.php b/composer/mikehaertl/php-pdftk/src/XfdfFile.php new file mode 100644 index 0000000000..c1be2d947b --- /dev/null +++ b/composer/mikehaertl/php-pdftk/src/XfdfFile.php @@ -0,0 +1,233 @@ + field value + * 'Firstname' => 'John', + * + * // Hierarchical/nested fields in dot notation + * 'Address.Street' => 'Some Street', + * 'Address.City' => 'Any City', + * + * // Multi value fields + * 'Pets' => ['Cat', 'Mouse'], + * ] + * ``` + * + * This will result in the following XML structure (header/footer omitted): + * + * ``` + * + * John + * + * + * + * Some Street + * + * + * Any City + * + * + * + * Cat + * Mouse + * + * ``` + * + * @author Tomas Holy + * @author Michael Härtl + * @license http://www.opensource.org/licenses/MIT + */ +class XfdfFile extends File +{ + // XFDF file header + const XFDF_HEADER = << + + + +FDF; + + // XFDF file footer + const XFDF_FOOTER = << + + +FDF; + + /** + * Constructor + * + * + * @param array $data the form data as name => value + * @param string|null $suffix the optional suffix for the tmp file + * @param string|null $prefix the optional prefix for the tmp file. If null + * 'php_tmpfile_' is used. + * @param string|null $directory directory where the file should be + * created. Autodetected if not provided. + * @param string|null $encoding of the data. Default is 'UTF-8'. + */ + public function __construct($data, $suffix = null, $prefix = null, $directory = null, $encoding = 'UTF-8') + { + if ($directory === null) { + $directory = self::getTempDir(); + } + if ($suffix === null) { + $suffix = '.xfdf'; + } + if ($prefix === null) { + $prefix = 'php_pdftk_xfdf_'; + } + + $tempfile = tempnam($directory, $prefix); + $this->_fileName = $tempfile . $suffix; + rename($tempfile, $this->_fileName); + + $fields = $this->parseData($data, $encoding); + $this->writeXml($fields); + } + + /** + * Parses an array of key/value data into a nested array structure. + * + * The data may use keys in dot notation (#55). Values can also be arrays in + * case of multi value fields (#148). To make both distinguishable in the + * result array keys that represent field names are prefixed with `_`. This + * also allows for numeric field names (#260). + * + * For example an array like this: + * + * ``` + * [ + * 'a' => 'value a', + * 'b.x' => 'value b.x', + * 'b.y' => 'value b.y', + * + * 'c.0' => 'val c.0', + * 'c.1' => 'val c.1', + * + * 'd' => ['m1', 'm2'], + * ] + * ``` + * + * Will become: + * + * ``` + * [ + * '_a' => 'value a', + * '_b' => [ + * '_x' => 'value b.x', + * '_y' => 'value b.y', + * ], + * '_c' => [ + * '_0' => 'value c.0', + * '_1' => 'value c.1', + * ], + * '_d' => [ + * // notice the missing underscore in the keys + * 0 => 'm1', + * 1 => 'm2', + * ], + * ] + * + * + * @param mixed $data the data to parse + * @param string the encoding of the data + * @return array the result array in UTF-8 encoding with dot keys converted + * to nested arrays + */ + protected function parseData($data, $encoding) + { + $result = array(); + foreach ($data as $key => $value) { + if ($encoding !== 'UTF-8' && function_exists('mb_convert_encoding')) { + $key = mb_convert_encoding($key, 'UTF-8', $encoding); + $value = mb_convert_encoding($value, 'UTF-8', $encoding); + } + if (strpos($key, '.') === false) { + $result['_' . $key] = $value; + } else { + $target = &$result; + $keyParts = explode('.', $key); + $lastPart = array_pop($keyParts); + foreach ($keyParts as $part) { + if (!isset($target['_' . $part])) { + $target['_' . $part] = array(); + } + $target = &$target['_' . $part]; + } + $target['_' . $lastPart] = $value; + } + } + return $result; + } + + /** + * Write the given fields to an XML file + * + * @param array $fields the fields in a nested array structure + */ + protected function writeXml($fields) + { + // Use fwrite, since file_put_contents() messes around with character encoding + $fp = fopen($this->_fileName, 'w'); + fwrite($fp, self::XFDF_HEADER); + $this->writeFields($fp, $fields); + fwrite($fp, self::XFDF_FOOTER); + fclose($fp); + } + + /** + * Write the fields to the given filepointer + * + * @param int $fp + * @param mixed[] $fields an array of field values as returned by + * `parseData()`. + */ + protected function writeFields($fp, $fields) + { + foreach ($fields as $key => $value) { + $key = $this->xmlEncode(substr($key,1)); + fwrite($fp, "\n"); + if (!is_array($value)) { + $value = array($value); + } + if (array_key_exists(0, $value)) { + // Numeric keys: single or multi-value field + foreach($value as $val) { + $val = $this->xmlEncode($val); + fwrite($fp, "$val\n"); + } + } else { + // String keys: nested/hierarchical fields + $this->writeFields($fp, $value); + } + fwrite($fp, "\n"); + } + } + + /** + * @param string|null $value the value to encode + * @return string|null the value correctly encoded for use in a XML document + */ + protected function xmlEncode($value) + { + if ($value === null) { + return null; + } + return defined('ENT_XML1') ? + htmlspecialchars($value, ENT_XML1, 'UTF-8') : + htmlspecialchars($value); + } +} diff --git a/composer/mikehaertl/php-shellcommand/.github/workflows/tests.yml b/composer/mikehaertl/php-shellcommand/.github/workflows/tests.yml new file mode 100644 index 0000000000..94c02ab72e --- /dev/null +++ b/composer/mikehaertl/php-shellcommand/.github/workflows/tests.yml @@ -0,0 +1,48 @@ +name: Tests +on: pull_request +jobs: + phpunit: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + matrix: + php: + - "5.3" + - "5.4" + - "5.5" + - "5.6" + - "7.0" + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + + - name: Update composer + run: composer self-update + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer packages + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run phpunit + run: vendor/bin/phpunit --color=always diff --git a/composer/mikehaertl/php-shellcommand/CHANGELOG.md b/composer/mikehaertl/php-shellcommand/CHANGELOG.md new file mode 100644 index 0000000000..c7c13e0008 --- /dev/null +++ b/composer/mikehaertl/php-shellcommand/CHANGELOG.md @@ -0,0 +1,102 @@ +# CHANGELOG + +## 1.6.4 + + * Let getExecCommand() not cache the created command string + +## 1.6.3 + + * Include PHP 5.3 in version requirements + +## 1.6.2 + + * Add .gitattributes to reduce package size + +## 1.6.1 + + * Issue #44 Fix potential security issue with escaping shell args (@Kirill89 / https://snyk.io/) + +## 1.6.0 + + * Issue #24 Implement timeout feature + +## 1.5.0 + + * Issue #20 Refactor handling of stdin/stdou/sterr streams with proc_open(). + By default these streams now operate in non-blocking mode which should fix + many hanging issues that were caused when the command received/sent a lot of + input/output. This is the new default on Non-Windows systems (it's not + supported on Windows, though). To get the old behavior the nonBlockingMode + option can be set to false. + +## 1.4.1 + + * Allow command names with spaces on Windows (@Robindfuller ) + +## 1.4.0 + + * Allow stdin to be a stream or a file handle (@Arzaroth) + +## 1.3.0 + + * Add setStdIn() which allows to pipe an input string to the command (@martinqvistgard) + +## 1.2.5 + + * Issue #22 Fix execution of relative file paths on windows + +## 1.2.4 + + * Reverted changes for Issue #20 as this introduced BC breaking problems + +## 1.2.3 + + * Issue #20: Read stderr before stdout to avoid hanging processes + +## 1.2.2 + + * Issue #16: Command on different drive didn't work on windows + +## 1.2.1 + + * Issue #1: Command with spaces didn't work on windows + +## 1.2.0 + + * Add option to return untrimmed output and error + +## 1.1.0 + + * Issue #7: UTF-8 encoded arguments where truncated + +## 1.0.7 + + * Issue #6: Solve `proc_open()` pipe configuration for both, Windows / Linux + +## 1.0.6 + + * Undid `proc_open()` changes as it broke error capturing + +## 1.0.5 + + * Improve `proc_open()` pipe configuration + +## 1.0.4 + + * Add `$useExec` option to fix Windows issues (#3) + +## 1.0.3 + + * Add `getExecuted()` to find out execution status of the command + +## 1.0.2 + + * Add `$escape` parameter to `addArg()` to override escaping settings per call + +## 1.0.1 + + * Minor fixes + +## 1.0.0 + + * Initial release diff --git a/composer/mikehaertl/php-shellcommand/LICENSE b/composer/mikehaertl/php-shellcommand/LICENSE new file mode 100644 index 0000000000..c60edfb5a1 --- /dev/null +++ b/composer/mikehaertl/php-shellcommand/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Michael Härtl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composer/mikehaertl/php-shellcommand/README.md b/composer/mikehaertl/php-shellcommand/README.md new file mode 100644 index 0000000000..eaa4039585 --- /dev/null +++ b/composer/mikehaertl/php-shellcommand/README.md @@ -0,0 +1,204 @@ +php-shellcommand +================ + +[![GitHub Tests](https://github.com/mikehaertl/php-shellcommand/workflows/Tests/badge.svg)](https://github.com/mikehaertl/php-shellcommand/actions) +[![Packagist Version](https://img.shields.io/packagist/v/mikehaertl/php-shellcommand?label=version)](https://packagist.org/packages/mikehaertl/php-shellcommand) +[![Packagist Downloads](https://img.shields.io/packagist/dt/mikehaertl/php-shellcommand)](https://packagist.org/packages/mikehaertl/php-shellcommand) +[![GitHub license](https://img.shields.io/github/license/mikehaertl/php-shellcommand)](https://github.com/mikehaertl/php-shellcommand/blob/master/LICENSE) +[![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/mikehaertl/php-shellcommand)](https://packagist.org/packages/mikehaertl/php-shellcommand) + +php-shellcommand provides a simple object oriented interface to execute shell commands. + +## Installing + +### Prerequisites + +Your php version must be `5.4` or later. + +### Installing with composer + +This package can be installed easily using composer. + +``` +composer require mikehaertl/php-shellcommand +``` + +## Features + + * Catches `stdOut`, `stdErr` and `exitCode` + * Handle argument escaping + * Pass environment vars and other options to `proc_open()` + * Pipe resources like files or streams into the command + * Timeout for execution + +## Examples + +### Basic Example + +```php +execute()) { + echo $command->getOutput(); +} else { + echo $command->getError(); + $exitCode = $command->getExitCode(); +} +``` + +### Advanced Features + +#### Add Arguments +```php +addArg('--name=', "d'Artagnan"); + +// Add argument with several values +// results in --keys key1 key2 +$command->addArg('--keys', ['key1','key2']); +``` + +### Pipe Input Into Command + +From string: +```php +setStdIn('{"foo": 0}'); +if (!$command->execute()) { + echo $command->getError(); +} else { + echo $command->getOutput(); +} +// Output: +// { +// "foo": 0 +// } +``` + +From file: +```php +setStdIn($fh); +if (!$command->execute()) { + echo $command->getError(); +} else { + echo $command->getOutput(); +} +fclose($fh); +``` +From URL: +```php +setStdIn($fh); +if (!$command->execute()) { + echo $command->getError(); +} else { + echo $command->getOutput(); +} +fclose($fh); +``` + +#### Set Command Instance Options +```php + '/usr/local/bin/mycommand', + + // Will be passed as environment variables to the command + 'procEnv' => [ + 'DEMOVAR' => 'demovalue' + ], + + // Will be passed as options to proc_open() + 'procOptions' => [ + 'bypass_shell' => true, + ], +]); +``` + +## API + +### Properties + + * `$escapeArgs`: Whether to escape any argument passed through `addArg()`. Default is `true`. + * `$escapeCommand`: Whether to escape the command passed to `setCommand()` or the constructor. + This is only useful if `$escapeArgs` is `false`. Default is `false`. + * `$useExec`: Whether to use `exec()` instead of `proc_open()`. This is a workaround for OS which + have problems with `proc_open()`. Default is `false`. + * `$captureStdErr`: Whether to capture stderr when `useExec` is set. This will try to redirect + the otherwhise unavailable `stderr` to `stdout`, so that both have the same content on error. + Default is `true`. + * `$procCwd`: The initial working dir passed to `proc_open()`. Default is `null` for current + PHP working dir. + * `$procEnv`: An array with environment variables to pass to `proc_open()`. Default is `null` for none. + * `$procOptions`: An array of `other_options` for `proc_open()`. Default is `null` for none. + * `$nonBlockingMode`: Whether to set the stdin/stdout/stderr streams to non-blocking + mode when `proc_open()` is used. This allows to have huge inputs/outputs + without making the process hang. The default is `null` which will enable + the feature on Non-Windows systems. Set it to `true` or `false` to manually + enable/disable it. Note that it doesn't work on Windows. + * `$timeout`: The time in seconds after which the command should be + terminated. This only works in non-blocking mode. Default is `null` which + means the process is never terminated. + * `$locale`: The locale to (temporarily) set with `setlocale()` before running the command. + This can be set to e.g. `en_US.UTF-8` if you have issues with UTF-8 encoded arguments. + +You can configure all these properties via an array that you pass in the constructor. You can also +pass `command`, `execCommand` and `args` as options. This will call the respective setter (`setCommand()`, +`setExecCommand()`, etc.). + +### Methods + + * `__construct($options = null)` + * `$options`: either a command string or an options array (see `setOptions()`) + * `__toString()`: The result from `getExecCommand()` + * `setOptions($options)`: Set command options + * `$options`: array of name => value options that should be applied to the object. + You can also pass options that use a setter, e.g. you can pass a `command` option which + will be passed to `setCommand().` + * `setCommand($command)`: Set command + * `$command`: The command or full command string to execute, like `gzip` or `gzip -d`. + You can still call `addArg()` to add more arguments to the command. If `$escapeCommand` was + set to `true`, the command gets escaped through `escapeshellcmd()`. + * `getCommand()`: The command that was set through `setCommand()` or passed to the constructor. + * `getExecCommand()`: The full command string to execute. + * `setArgs($args)`: Set argument as string + * `$args`: The command arguments as string. Note, that these will not get escaped. This + will overwrite the args added with `addArgs()`. + * `getArgs()`: The command arguments that where set through `setArgs()` or `addArg()`, as string + * `addArg($key, $value=null, $escape=null)`: Add argument with correct escaping + * `$key`: The argument key to add e.g. `--feature` or `--name=`. If the key does not end with + and `=`, the (optional) `$value` will be separated by a space. The key will get + escaped if `$escapeArgs` is `true`. + * `$value`: The optional argument value which will get escaped if `$escapeArgs` is `true`. + An array can be passed to add more than one value for a key, e.g. `addArg('--exclude', ['val1','val2'])` + which will create the option "--exclude 'val1' 'val2'". + * `$escape`: If set, this overrides the `$escapeArgs` setting and enforces escaping/no escaping + * `setStdIn()`: String or resource to supply to command via standard input. + This enables the same functionality as piping on the command line. It can + also be a resource like a file handle or a stream in which case its content + will be piped into the command like an input redirection. + * `getOutput()`: The command output as string. Empty if none. + * `getError()`: The error message, either stderr or internal message. Empty if no error. + * `getStdErr()`: The stderr output. Empty if none. + * `getExitCode()`: The exit code or `null` if command was not executed. + * `getExecuted()`: Whether the command was successfully executed. + * `getIsWindows()`: Whether we are on a Windows Owe are on a Windows OS + * `execute()`: Executes the command and returns `true` on success, `false` otherwhise. + +> **Note:** `getError()`, `getStdErr()` and `getOutput()` return the trimmed output. +> You can pass `false` to these methods if you need any possible line breaks at the end. diff --git a/composer/mikehaertl/php-shellcommand/composer.json b/composer/mikehaertl/php-shellcommand/composer.json new file mode 100644 index 0000000000..a638fca654 --- /dev/null +++ b/composer/mikehaertl/php-shellcommand/composer.json @@ -0,0 +1,28 @@ +{ + "name": "mikehaertl/php-shellcommand", + "description": "An object oriented interface to shell commands", + "keywords": ["shell"], + "license": "MIT", + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "require": { + "php": ">= 5.3.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0 <=9.4" + }, + "autoload": { + "psr-4": { + "mikehaertl\\shellcommand\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "tests\\": "tests" + } + } +} diff --git a/composer/mikehaertl/php-shellcommand/src/Command.php b/composer/mikehaertl/php-shellcommand/src/Command.php new file mode 100644 index 0000000000..963ced9689 --- /dev/null +++ b/composer/mikehaertl/php-shellcommand/src/Command.php @@ -0,0 +1,568 @@ +addArg('--name=', "d'Artagnan"); + * if ($command->execute()) { + * echo $command->getOutput(); + * } else { + * echo $command->getError(); + * $exitCode = $command->getExitCode(); + * } + * ``` + * + * @author Michael Härtl + * @license http://www.opensource.org/licenses/MIT + */ +class Command +{ + /** + * @var bool whether to escape any argument passed through `addArg()`. + * Default is `true`. + */ + public $escapeArgs = true; + + /** + * @var bool whether to escape the command passed to `setCommand()` or the + * constructor. This is only useful if `$escapeArgs` is `false`. Default + * is `false`. + */ + public $escapeCommand = false; + + /** + * @var bool whether to use `exec()` instead of `proc_open()`. This can be + * used on Windows system to workaround some quirks there. Note, that any + * errors from your command will be output directly to the PHP output + * stream. `getStdErr()` will also not work anymore and thus you also won't + * get the error output from `getError()` in this case. You also can't pass + * any environment variables to the command if this is enabled. Default is + * `false`. + */ + public $useExec = false; + + /** + * @var bool whether to capture stderr (2>&1) when `useExec` is true. This + * will try to redirect the stderr to stdout and provide the complete + * output of both in `getStdErr()` and `getError()`. Default is `true`. + */ + public $captureStdErr = true; + + /** + * @var string|null the initial working dir for `proc_open()`. Default is + * `null` for current PHP working dir. + */ + public $procCwd; + + /** + * @var array|null an array with environment variables to pass to + * `proc_open()`. Default is `null` for none. + */ + public $procEnv; + + /** + * @var array|null an array of other_options for `proc_open()`. Default is + * `null` for none. + */ + public $procOptions; + + /** + * @var bool|null whether to set the stdin/stdout/stderr streams to + * non-blocking mode when `proc_open()` is used. This allows to have huge + * inputs/outputs without making the process hang. The default is `null` + * which will enable the feature on Non-Windows systems. Set it to `true` + * or `false` to manually enable/disable it. It does not work on Windows. + */ + public $nonBlockingMode; + + /** + * @var int the time in seconds after which a command should be terminated. + * This only works in non-blocking mode. Default is `null` which means the + * process is never terminated. + */ + public $timeout; + + /** + * @var null|string the locale to temporarily set before calling + * `escapeshellargs()`. Default is `null` for none. + */ + public $locale; + + /** + * @var null|string|resource to pipe to standard input + */ + protected $_stdIn; + + /** + * @var string the command to execute + */ + protected $_command; + + /** + * @var array the list of command arguments + */ + protected $_args = array(); + + /** + * @var string the stdout output + */ + protected $_stdOut = ''; + + /** + * @var string the stderr output + */ + protected $_stdErr = ''; + + /** + * @var int the exit code + */ + protected $_exitCode; + + /** + * @var string the error message + */ + protected $_error = ''; + + /** + * @var bool whether the command was successfully executed + */ + protected $_executed = false; + + /** + * @param string|array $options either a command string or an options array + * @see setOptions + */ + public function __construct($options = null) + { + if (is_array($options)) { + $this->setOptions($options); + } elseif (is_string($options)) { + $this->setCommand($options); + } + } + + /** + * @param array $options array of name => value options (i.e. public + * properties) that should be applied to this object. You can also pass + * options that use a setter, e.g. you can pass a `fileName` option which + * will be passed to `setFileName()`. + * @throws \Exception on unknown option keys + * @return static for method chaining + */ + public function setOptions($options) + { + foreach ($options as $key => $value) { + if (property_exists($this, $key)) { + $this->$key = $value; + } else { + $method = 'set'.ucfirst($key); + if (method_exists($this, $method)) { + call_user_func(array($this,$method), $value); + } else { + throw new \Exception("Unknown configuration option '$key'"); + } + } + } + return $this; + } + + /** + * @param string $command the command or full command string to execute, + * like 'gzip' or 'gzip -d'. You can still call addArg() to add more + * arguments to the command. If `$escapeCommand` was set to true, the command + * gets escaped with `escapeshellcmd()`. + * @return static for method chaining + */ + public function setCommand($command) + { + if ($this->escapeCommand) { + $command = escapeshellcmd($command); + } + if ($this->getIsWindows()) { + // Make sure to switch to correct drive like "E:" first if we have + // a full path in command + if (isset($command[1]) && $command[1] === ':') { + $position = 1; + // Could be a quoted absolute path because of spaces. + // i.e. "C:\Program Files (x86)\file.exe" + } elseif (isset($command[2]) && $command[2] === ':') { + $position = 2; + } else { + $position = false; + } + + // Absolute path. If it's a relative path, let it slide. + if ($position) { + $command = sprintf( + $command[$position - 1] . ': && cd %s && %s', + escapeshellarg(dirname($command)), + escapeshellarg(basename($command)) + ); + } + } + $this->_command = $command; + return $this; + } + + /** + * @param string|resource $stdIn If set, the string will be piped to the + * command via standard input. This enables the same functionality as + * piping on the command line. It can also be a resource like a file + * handle or a stream in which case its content will be piped into the + * command like an input redirection. + * @return static for method chaining + */ + public function setStdIn($stdIn) { + $this->_stdIn = $stdIn; + return $this; + } + + /** + * @return string|null the command that was set through `setCommand()` or + * passed to the constructor. `null` if none. + */ + public function getCommand() + { + return $this->_command; + } + + /** + * @return string|bool the full command string to execute. If no command + * was set with `setCommand()` or passed to the constructor it will return + * `false`. + */ + public function getExecCommand() + { + $command = $this->getCommand(); + if (!$command) { + $this->_error = 'Could not locate any executable command'; + return false; + } + + $args = $this->getArgs(); + return $args ? $command.' '.$args : $command; + } + + /** + * @param string $args the command arguments as string like `'--arg1=value1 + * --arg2=value2'`. Note that this string will not get escaped. This will + * overwrite the args added with `addArgs()`. + * @return static for method chaining + */ + public function setArgs($args) + { + $this->_args = array($args); + return $this; + } + + /** + * @return string the command args that where set with `setArgs()` or added + * with `addArg()` separated by spaces. + */ + public function getArgs() + { + return implode(' ', $this->_args); + } + + /** + * @param string $key the argument key to add e.g. `--feature` or + * `--name=`. If the key does not end with `=`, the (optional) $value will + * be separated by a space. The key will get escaped if `$escapeArgs` is `true`. + * @param string|array|null $value the optional argument value which will + * get escaped if $escapeArgs is true. An array can be passed to add more + * than one value for a key, e.g. + * `addArg('--exclude', array('val1','val2'))` + * which will create the option + * `'--exclude' 'val1' 'val2'`. + * @param bool|null $escape if set, this overrides the `$escapeArgs` setting + * and enforces escaping/no escaping of keys and values + * @return static for method chaining + */ + public function addArg($key, $value = null, $escape = null) + { + $doEscape = $escape !== null ? $escape : $this->escapeArgs; + $useLocale = $doEscape && $this->locale !== null; + + if ($useLocale) { + $locale = setlocale(LC_CTYPE, 0); // Returns current locale setting + setlocale(LC_CTYPE, $this->locale); + } + if ($value === null) { + $this->_args[] = $doEscape ? escapeshellarg($key) : $key; + } else { + if (substr($key, -1) === '=') { + $separator = '='; + $argKey = substr($key, 0, -1); + } else { + $separator = ' '; + $argKey = $key; + } + $argKey = $doEscape ? escapeshellarg($argKey) : $argKey; + + if (is_array($value)) { + $params = array(); + foreach ($value as $v) { + $params[] = $doEscape ? escapeshellarg($v) : $v; + } + $this->_args[] = $argKey . $separator . implode(' ', $params); + } else { + $this->_args[] = $argKey . $separator . + ($doEscape ? escapeshellarg($value) : $value); + } + } + if ($useLocale) { + setlocale(LC_CTYPE, $locale); + } + + return $this; + } + + /** + * @param bool $trim whether to `trim()` the return value. The default is `true`. + * @param string $characters the list of characters to trim. The default + * is ` \t\n\r\0\v\f`. + * @return string the command output (stdout). Empty if none. + */ + public function getOutput($trim = true, $characters = " \t\n\r\0\v\f") + { + return $trim ? trim($this->_stdOut, $characters) : $this->_stdOut; + } + + /** + * @param bool $trim whether to `trim()` the return value. The default is `true`. + * @param string $characters the list of characters to trim. The default + * is ` \t\n\r\0\v\f`. + * @return string the error message, either stderr or an internal message. + * Empty string if none. + */ + public function getError($trim = true, $characters = " \t\n\r\0\v\f") + { + return $trim ? trim($this->_error, $characters) : $this->_error; + } + + /** + * @param bool $trim whether to `trim()` the return value. The default is `true`. + * @param string $characters the list of characters to trim. The default + * is ` \t\n\r\0\v\f`. + * @return string the stderr output. Empty if none. + */ + public function getStdErr($trim = true, $characters = " \t\n\r\0\v\f") + { + return $trim ? trim($this->_stdErr, $characters) : $this->_stdErr; + } + + /** + * @return int|null the exit code or null if command was not executed yet + */ + public function getExitCode() + { + return $this->_exitCode; + } + + /** + * @return string whether the command was successfully executed + */ + public function getExecuted() + { + return $this->_executed; + } + + /** + * Execute the command + * + * @return bool whether execution was successful. If `false`, error details + * can be obtained from `getError()`, `getStdErr()` and `getExitCode()`. + */ + public function execute() + { + $command = $this->getExecCommand(); + + if (!$command) { + return false; + } + + if ($this->useExec) { + $execCommand = $this->captureStdErr ? "$command 2>&1" : $command; + exec($execCommand, $output, $this->_exitCode); + $this->_stdOut = implode("\n", $output); + if ($this->_exitCode !== 0) { + $this->_stdErr = $this->_stdOut; + $this->_error = empty($this->_stdErr) ? 'Command failed' : $this->_stdErr; + return false; + } + } else { + $isInputStream = $this->_stdIn !== null && + is_resource($this->_stdIn) && + in_array(get_resource_type($this->_stdIn), array('file', 'stream')); + $isInputString = is_string($this->_stdIn); + $hasInput = $isInputStream || $isInputString; + $hasTimeout = $this->timeout !== null && $this->timeout > 0; + + $descriptors = array( + 1 => array('pipe','w'), + 2 => array('pipe', $this->getIsWindows() ? 'a' : 'w'), + ); + if ($hasInput) { + $descriptors[0] = array('pipe', 'r'); + } + + + // Issue #20 Set non-blocking mode to fix hanging processes + $nonBlocking = $this->nonBlockingMode === null ? + !$this->getIsWindows() : $this->nonBlockingMode; + + $startTime = $hasTimeout ? time() : 0; + $process = proc_open($command, $descriptors, $pipes, $this->procCwd, $this->procEnv, $this->procOptions); + + if (is_resource($process)) { + + if ($nonBlocking) { + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + if ($hasInput) { + $writtenBytes = 0; + $isInputOpen = true; + stream_set_blocking($pipes[0], false); + if ($isInputStream) { + stream_set_blocking($this->_stdIn, false); + } + } + + // Due to the non-blocking streams we now have to check in + // a loop if the process is still running. We also need to + // ensure that all the pipes are written/read alternately + // until there's nothing left to write/read. + $isRunning = true; + while ($isRunning) { + $status = proc_get_status($process); + $isRunning = $status['running']; + + // We first write to stdIn if we have an input. For big + // inputs it will only write until the input buffer of + // the command is full (the command may now wait that + // we read the output buffers - see below). So we may + // have to continue writing in another cycle. + // + // After everything is written it's safe to close the + // input pipe. + if ($isRunning && $hasInput && $isInputOpen) { + if ($isInputStream) { + $written = stream_copy_to_stream($this->_stdIn, $pipes[0], 16 * 1024, $writtenBytes); + if ($written === false || $written === 0) { + $isInputOpen = false; + fclose($pipes[0]); + } else { + $writtenBytes += $written; + } + } else { + if ($writtenBytes < strlen($this->_stdIn)) { + $writtenBytes += fwrite($pipes[0], substr($this->_stdIn, $writtenBytes)); + } else { + $isInputOpen = false; + fclose($pipes[0]); + } + } + } + + // Read out the output buffers because if they are full + // the command may block execution. We do this even if + // $isRunning is `false`, because there could be output + // left in the buffers. + // + // The latter is only an assumption and needs to be + // verified - but it does not hurt either and works as + // expected. + // + while (($out = fgets($pipes[1])) !== false) { + $this->_stdOut .= $out; + } + while (($err = fgets($pipes[2])) !== false) { + $this->_stdErr .= $err; + } + + $runTime = $hasTimeout ? time() - $startTime : 0; + if ($isRunning && $hasTimeout && $runTime >= $this->timeout) { + // Only send a SIGTERM and handle status in the next cycle + proc_terminate($process); + } + + if (!$isRunning) { + $this->_exitCode = $status['exitcode']; + if ($this->_exitCode !== 0 && empty($this->_stdErr)) { + if ($status['stopped']) { + $signal = $status['stopsig']; + $this->_stdErr = "Command stopped by signal $signal"; + } elseif ($status['signaled']) { + $signal = $status['termsig']; + $this->_stdErr = "Command terminated by signal $signal"; + } else { + $this->_stdErr = 'Command unexpectedly terminated without error message'; + } + } + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + } else { + // The command is still running. Let's wait some + // time before we start the next cycle. + usleep(10000); + } + } + } else { + if ($hasInput) { + if ($isInputStream) { + stream_copy_to_stream($this->_stdIn, $pipes[0]); + } elseif ($isInputString) { + fwrite($pipes[0], $this->_stdIn); + } + fclose($pipes[0]); + } + $this->_stdOut = stream_get_contents($pipes[1]); + $this->_stdErr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $this->_exitCode = proc_close($process); + } + + if ($this->_exitCode !== 0) { + $this->_error = $this->_stdErr ? + $this->_stdErr : + "Failed without error message: $command (Exit code: {$this->_exitCode})"; + return false; + } + } else { + $this->_error = "Could not run command $command"; + return false; + } + } + + $this->_executed = true; + + return true; + } + + /** + * @return bool whether we are on a Windows OS + */ + public function getIsWindows() + { + return strncasecmp(PHP_OS, 'WIN', 3)===0; + } + + /** + * @return string the current command string to execute + */ + public function __toString() + { + return (string) $this->getExecCommand(); + } +} diff --git a/composer/mikehaertl/php-tmpfile/.github/workflows/tests.yml b/composer/mikehaertl/php-tmpfile/.github/workflows/tests.yml new file mode 100644 index 0000000000..94c02ab72e --- /dev/null +++ b/composer/mikehaertl/php-tmpfile/.github/workflows/tests.yml @@ -0,0 +1,48 @@ +name: Tests +on: pull_request +jobs: + phpunit: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + matrix: + php: + - "5.3" + - "5.4" + - "5.5" + - "5.6" + - "7.0" + - "7.1" + - "7.2" + - "7.3" + - "7.4" + - "8.0" + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + + - name: Update composer + run: composer self-update + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install composer packages + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run phpunit + run: vendor/bin/phpunit --color=always diff --git a/composer/mikehaertl/php-tmpfile/LICENSE b/composer/mikehaertl/php-tmpfile/LICENSE new file mode 100644 index 0000000000..28dbb2a292 --- /dev/null +++ b/composer/mikehaertl/php-tmpfile/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Michael Härtl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/composer/mikehaertl/php-tmpfile/README.md b/composer/mikehaertl/php-tmpfile/README.md new file mode 100644 index 0000000000..86d7b4a56b --- /dev/null +++ b/composer/mikehaertl/php-tmpfile/README.md @@ -0,0 +1,64 @@ +php-tmpfile +=========== + +[![GitHub Tests](https://github.com/mikehaertl/php-tmpfile/workflows/Tests/badge.svg)](https://github.com/mikehaertl/php-tmpfile/actions) +[![Packagist Version](https://img.shields.io/packagist/v/mikehaertl/php-tmpfile?label=version)](https://packagist.org/packages/mikehaertl/php-tmpfile) +[![Packagist Downloads](https://img.shields.io/packagist/dt/mikehaertl/php-tmpfile)](https://packagist.org/packages/mikehaertl/php-tmpfile) +[![GitHub license](https://img.shields.io/github/license/mikehaertl/php-tmpfile)](https://github.com/mikehaertl/php-tmpfile/blob/master/LICENSE) + +A convenience class for temporary files. + +## Features + + * Create temporary file with arbitrary content + * Delete file after use (can be disabled) + * Send file to client, either inline or with save dialog, optionally with custom HTTP headers + * Save file locally + +## Examples + +```php +send('home.html'); +// ... with custom content type (autodetected otherwhise) +$file->send('home.html', 'application/pdf'); +// ... for inline display (download dialog otherwhise) +$file->send('home.html', 'application/pdf', true); +// ... with custom headers +$file->send('home.html', 'application/pdf', true, [ + 'X-Header' => 'Example', +]); + +// save to disk +$file->saveAs('/dir/test.html'); + +// Access file name and directory +echo $file->getFileName(); +echo $file->getTempDir(); +``` + +If you want to keep the temporary file, e.g. for debugging, you can set the `$delete` property to false: + +```php +delete = false; +``` + +Default HTTP headers can also be added: +```php +send('home.html'); +``` diff --git a/composer/mikehaertl/php-tmpfile/composer.json b/composer/mikehaertl/php-tmpfile/composer.json new file mode 100644 index 0000000000..c28c46a3ec --- /dev/null +++ b/composer/mikehaertl/php-tmpfile/composer.json @@ -0,0 +1,26 @@ +{ + "name": "mikehaertl/php-tmpfile", + "description": "A convenience class for temporary files", + "keywords": ["files"], + "license": "MIT", + "authors": [ + { + "name": "Michael Härtl", + "email": "haertl.mike@gmail.com" + } + ], + "require-dev": { + "php": ">=5.3.0", + "phpunit/phpunit": ">4.0 <=9.4" + }, + "autoload": { + "psr-4": { + "mikehaertl\\tmp\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "tests\\": "tests" + } + } +} diff --git a/composer/mikehaertl/php-tmpfile/src/File.php b/composer/mikehaertl/php-tmpfile/src/File.php new file mode 100644 index 0000000000..2271260195 --- /dev/null +++ b/composer/mikehaertl/php-tmpfile/src/File.php @@ -0,0 +1,197 @@ + + * @license http://www.opensource.org/licenses/MIT + */ +class File +{ + const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; + + /** + * @var bool whether to delete the tmp file when it's no longer referenced + * or when the request ends. Default is `true`. + */ + public $delete = true; + + /** + * @var array the list of static default headers to send when `send()` is + * called as key/value pairs. + */ + public static $defaultHeaders = array( + 'Pragma' => 'public', + 'Expires' => 0, + 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', + 'Content-Transfer-Encoding' => 'binary', + ); + + /** + * @var string the name of this file + */ + protected $_fileName; + + /** + * Constructor + * + * @param string $content the tmp file content + * @param string|null $suffix the optional suffix for the tmp file + * @param string|null $prefix the optional prefix for the tmp file. If null + * 'php_tmpfile_' is used. + * @param string|null $directory directory where the file should be + * created. Autodetected if not provided. + */ + public function __construct($content, $suffix = null, $prefix = null, $directory = null) + { + if ($directory === null) { + $directory = self::getTempDir(); + } + + if ($prefix === null) { + $prefix = 'php_tmpfile_'; + } + + $this->_fileName = tempnam($directory,$prefix); + if ($suffix !== null) { + $newName = $this->_fileName . $suffix; + rename($this->_fileName, $newName); + $this->_fileName = $newName; + } + file_put_contents($this->_fileName, $content); + } + + /** + * Delete tmp file on shutdown if `$delete` is `true` + */ + public function __destruct() + { + if ($this->delete && file_exists($this->_fileName)) { + unlink($this->_fileName); + } + } + + /** + * Send tmp file to client, either inline or as download + * + * @param string|null $filename the filename to send. If empty, the file is + * streamed inline. + * @param string|null $contentType the Content-Type header to send. If + * `null` the type is auto-detected and if that fails + * 'application/octet-stream' is used. + * @param bool $inline whether to force inline display of the file, even if + * filename is present. + * @param array $headers a list of additional HTTP headers to send in the + * response as an array. The array keys are the header names like + * 'Cache-Control' and the array values the header value strings to send. + * Each array value can also be another array of strings if the same header + * should be sent multiple times. This can also be used to override + * automatically created headers like 'Expires' or 'Content-Length'. To suppress + * automatically created headers, `false` can also be used as header value. + */ + public function send($filename = null, $contentType = null, $inline = false, $headers = array()) + { + $headers = array_merge(self::$defaultHeaders, $headers); + + if ($contentType !== null) { + $headers['Content-Type'] = $contentType; + } elseif (!isset($headers['Content-Type'])) { + $contentType = @mime_content_type($this->_filename); + if ($contentType === false) { + $contentType = self::DEFAULT_CONTENT_TYPE; + } + $headers['Content-Type'] = $contentType; + } + + if (!isset($headers['Content-Length'])) { + // #11 Undefined index: HTTP_USER_AGENT + $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; + + // #84: Content-Length leads to "network connection was lost" on iOS + $isIOS = preg_match('/i(phone|pad|pod)/i', $userAgent); + if (!$isIOS) { + $headers['Content-Length'] = filesize($this->_fileName); + } + } + + if (($filename !== null || $inline) && !isset($headers['Content-Disposition'])) { + $disposition = $inline ? 'inline' : 'attachment'; + $encodedFilename = rawurlencode($filename); + $headers['Content-Disposition'] = "$disposition; " . + "filename=\"$filename\"; " . + "filename*=UTF-8''$encodedFilename"; + } + + $this->sendHeaders($headers); + readfile($this->_fileName); + } + + /** + * @param string $name the name to save the file as + * @return bool whether the file could be saved + */ + public function saveAs($name) + { + return copy($this->_fileName, $name); + } + + /** + * @return string the full file name + */ + public function getFileName() + { + return $this->_fileName; + } + + /** + * @return string the path to the temp directory + */ + public static function getTempDir() + { + if (function_exists('sys_get_temp_dir')) { + return sys_get_temp_dir(); + } elseif ( + ($tmp = getenv('TMP')) || + ($tmp = getenv('TEMP')) || + ($tmp = getenv('TMPDIR')) + ) { + return realpath($tmp); + } else { + return '/tmp'; + } + } + + /** + * @return string the full file name + */ + public function __toString() + { + return $this->_fileName; + } + + /** + * Send the given list of headers + * + * @param array $headers the list of headers to send as key/value pairs. + * Value can either be a string or an array of strings to send the same + * header multiple times. + */ + protected function sendHeaders($headers) + { + foreach ($headers as $name => $value) { + if ($value === false) { + continue; + } + if (is_array($value)) { + foreach ($value as $v) { + header("$name: $v"); + } + } else { + header("$name: $value"); + } + } + } +} From 63b6d9d0473c6bf9864f21d4f73f538ff5745d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 1 Aug 2024 16:15:18 +0200 Subject: [PATCH 3/3] fix: Properly apply field filling with the same structure as for office documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl [skip ci] --- lib/Controller/TemplateFieldController.php | 4 +-- lib/Service/PdfService.php | 38 ++++++++++++++++++---- lib/Service/TemplateFieldService.php | 34 ++++++++++++++----- psalm.xml | 1 + 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/lib/Controller/TemplateFieldController.php b/lib/Controller/TemplateFieldController.php index be81e9b78f..5071ba43b2 100644 --- a/lib/Controller/TemplateFieldController.php +++ b/lib/Controller/TemplateFieldController.php @@ -60,9 +60,9 @@ public function extractFields(int $fileId): DataResponse { * @return DataResponse */ #[NoAdminRequired] - public function fillFields(int $fileId, array $fields): DataResponse { + public function fillFields(int $fileId, array $fields, ?string $destination = null): DataResponse { try { - $this->templateFieldService->fillFields($fileId, $fields); + $this->templateFieldService->fillFields($fileId, $fields, $destination); return new DataResponse([], Http::STATUS_OK); } catch (\Exception $e) { diff --git a/lib/Service/PdfService.php b/lib/Service/PdfService.php index 6945f5e051..0e7a1d9cb3 100644 --- a/lib/Service/PdfService.php +++ b/lib/Service/PdfService.php @@ -24,20 +24,21 @@ public function extractFields(Node $file): array { try { $pdf = new Pdf($filePath); - $fields = $pdf->getDataFields(); + $fields = $pdf->getDataFields() ?: []; $templateFields = []; $index = 0; foreach ($fields as $field) { - $fieldType = FieldType::tryFrom($field['FieldType']) ?? null; + $fieldType = self::matchFieldType($field['FieldType']); if ($fieldType === null) { continue; } $templateFields[] = new Field( - $index, - $field['FieldName'], + (string)$index, + $field['FieldValue'], $fieldType, + alias: $field['FieldName'], ); $index++; } @@ -48,17 +49,42 @@ public function extractFields(Node $file): array { } } - public function fillFields(Node $file, array $fieldValues): void { + public function fillFields(Node $file, array $fieldValues) { + if (!$file instanceof \OCP\Files\File) { + return; + } + $filePath = $file->getStorage()->getLocalFile($file->getInternalPath()); try { $pdf = new Pdf($filePath); - $pdf->fillForm($fieldValues); + $fields = $pdf->getDataFields(); + $fillData = []; + foreach ($fieldValues as $index => $field) { + if (!isset($fields[$index])) { + continue; + } + $fieldName = $fields[$index]['FieldName']; + $fieldData = $field['content'] ?? $fields[$index]['FieldValue']; + $fillData[$fieldName] = $fieldData; + } + unset($pdf); + + $pdf = new Pdf($filePath); + $pdf->fillForm($fillData); $pdf->flatten(); $pdf->saveAs($filePath); + return file_get_contents($filePath); } catch (\Exception $e) { $this->logger->error('Failed to fill fields in PDF: {error}', ['error' => $e->getMessage(), 'exception' => $e]); throw $e; } } + + public static function matchFieldType(string $type): ?FieldType { + return match ($type) { + 'Text' => FieldType::RichText, + default => null + }; + } } diff --git a/lib/Service/TemplateFieldService.php b/lib/Service/TemplateFieldService.php index 620f2fddf3..5e0aafb529 100644 --- a/lib/Service/TemplateFieldService.php +++ b/lib/Service/TemplateFieldService.php @@ -27,14 +27,16 @@ public function __construct( private LoggerInterface $logger, private ICacheFactory $cacheFactory, private PdfService $pdfService, + private ?string $userId, ) { } /** * @param Node|int $file - * @return array|string + * @return Field[] + * @throws NotFoundException */ - public function extractFields(Node|int $file) { + public function extractFields(Node|int $file): array { if (!$this->capabilitiesService->hasFormFilling()) { return []; } @@ -49,7 +51,7 @@ public function extractFields(Node|int $file) { $cachedResponse = $localCache->get($cacheName); if ($cachedResponse !== null) { - return $cachedResponse; + // return $cachedResponse; } if ($file->getMimeType() === 'application/pdf') { @@ -58,6 +60,10 @@ public function extractFields(Node|int $file) { return $fields; } + if (!in_array($file->getMimetype(), Capabilities::MIMETYPES)) { + return []; + } + $collaboraUrl = $this->appConfig->getCollaboraUrlInternal(); $httpClient = $this->clientService->newClient(); @@ -107,10 +113,10 @@ public function extractFields(Node|int $file) { /** * @param Node|int $file - * @param array $fields + * @param array $fields * @return string|resource */ - public function fillFields(Node|int $file, array $fields = []) { + public function fillFields(Node|int $file, array $fields = [], ?string $destination = null) { if (!$this->capabilitiesService->hasFormFilling()) { throw new \RuntimeException('Form filling not supported by the Collabora server'); } @@ -120,8 +126,11 @@ public function fillFields(Node|int $file, array $fields = []) { } if ($file->getMimeType() === 'application/pdf') { - $this->pdfService->fillFields($file, $fields); - return ''; + $content = $this->pdfService->fillFields($file, $fields); + if ($destination !== null) { + $this->writeToDestination($destination, $content); + } + return $content; } $collaboraUrl = $this->appConfig->getCollaboraUrlInternal(); @@ -152,10 +161,19 @@ public function fillFields(Node|int $file, array $fields = []) { $form ); - return $response->getBody(); + $content = $response->getBody(); + if ($destination !== null) { + $this->writeToDestination($destination, $content); + } + return $content; } catch (\Exception $e) { $this->logger->error($e->getMessage()); throw $e; } } + + private function writeToDestination(string $destination, $data): void { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $userFolder->newFile($destination, $data); + } } diff --git a/psalm.xml b/psalm.xml index 8f766ba7a8..e4ecde8a32 100644 --- a/psalm.xml +++ b/psalm.xml @@ -21,6 +21,7 @@ +