chore: Move to actual runtime dependency file

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl 2024-08-01 16:13:58 +02:00
parent 2aaeac4f7c
commit 7491f79bb5
No known key found for this signature in database
GPG key ID: 4C614C6ED2CDE6DF
34 changed files with 4183 additions and 232 deletions

View file

@ -10,8 +10,7 @@
},
"require": {
"ext-json": "*",
"ext-simplexml": "*",
"mikehaertl/php-pdftk": "^0.13.1"
"ext-simplexml": "*"
},
"require-dev": {
"roave/security-advisories": "dev-master",

146
composer.lock generated
View file

@ -4,148 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f00345e82d316ea492b86fb74fb09c84",
"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"
}
],
"content-hash": "f118a358162577fab7ee75cad4ef1383",
"packages": [],
"packages-dev": [
{
"name": "doctrine/instantiator",
@ -3121,5 +2981,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View file

@ -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"
}
}

146
composer/composer.lock generated
View file

@ -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"
}

View file

@ -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<string, array<string, int>>
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
@ -81,8 +80,7 @@ class ClassLoader
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
* @var array<string, string>
*/
private $classMap = array();
@ -90,21 +88,20 @@ class ClassLoader
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
/** @var string|null */
private $apcuPrefix;
/**
* @var self[]
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
@ -113,7 +110,7 @@ class ClassLoader
}
/**
* @return string[]
* @return array<string, list<string>>
*/
public function getPrefixes()
{
@ -125,8 +122,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
@ -134,8 +130,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, string>
* @return list<string>
*/
public function getFallbackDirs()
{
@ -143,8 +138,7 @@ class ClassLoader
}
/**
* @return array[]
* @psalm-return array<string, string>
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
@ -152,8 +146,7 @@ class ClassLoader
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
@ -161,8 +154,7 @@ class ClassLoader
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
@ -179,24 +171,25 @@ class ClassLoader
* 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>|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 @@ class ClassLoader
$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 @@ class ClassLoader
* 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>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
@ -236,17 +229,18 @@ class ClassLoader
*/
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 @@ class ClassLoader
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 @@ class ClassLoader
* 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>|string $paths The PSR-0 base directories
*
* @return void
*/
@ -294,8 +288,8 @@ class ClassLoader
* 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>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
@ -481,9 +475,9 @@ class ClassLoader
}
/**
* 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<string, self>
*/
public static function getRegisteredLoaders()
{

View file

@ -77,8 +77,8 @@ return array(
'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 @@ return array(
'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',
);

View file

@ -6,7 +6,8 @@ $vendorDir = dirname(__DIR__);
$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'),
);

View file

@ -22,6 +22,8 @@ class ComposerAutoloaderInitRichdocuments
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'));

View file

@ -7,27 +7,32 @@ namespace Composer\Autoload;
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)

View file

@ -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": []
}

View file

@ -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,
),
),
);

View file

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 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
);
}

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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"
}
}
}

View file

@ -0,0 +1,249 @@
<?php
namespace mikehaertl\pdftk;
use mikehaertl\shellcommand\Command as BaseCommand;
/**
* Command
*
* This class represents an pdftk shell command. It extends a standard
* shellcommand and adds pdftk specific features to add options and operations.
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @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');
}
}
}

View file

@ -0,0 +1,166 @@
<?php
namespace mikehaertl\pdftk;
use ArrayObject;
/**
* This class is an array representation of the dump_data_fields output of
* pdftk.
*
* @author Ray Holland <raymondaholland+php-pdftk@gmail.com>
* @author Michael Härtl <haertl.mike@gmail.com>
* @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]);
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace mikehaertl\pdftk;
use mikehaertl\tmp\File;
/**
* FdfFile
*
* This class represents a temporary FDF (1.2) file that can be used to fill a
* PDF form with valid unicode characters.
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @license http://www.opensource.org/licenses/MIT
*/
class FdfFile extends File
{
// FDF file header
const FDF_HEADER = <<<FDF
%FDF-1.2
1 0 obj<</FDF<< /Fields[
FDF;
// FDF file footer
const FDF_FOOTER = <<<FDF
] >> >>
endobj
trailer
<</Root 1 0 R>>
%%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 .= "<</T(" . chr(0xFE) . chr(0xFF) . $utf16Key . ")/V(" . chr(0xFE) . chr(0xFF) . $utf16Value . ")>>\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);
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace mikehaertl\pdftk;
use ArrayObject;
/**
* Class InfoFields
* Derived from DataFields
*
* @author Burak USGURLU <burak@uskur.com.tr>
* @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;
}
}

View file

@ -0,0 +1,190 @@
<?php
namespace mikehaertl\pdftk;
use Exception;
use mikehaertl\tmp\File;
/**
* InfoFile
*
* This class represents a temporary dump_data compatible file that can be used to update meta data of PDF
* with valid unicode characters.
*
* @author Burak Usgurlu <burak@uskur.com.tr>
* @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";
}
}
}

View file

@ -0,0 +1,749 @@
<?php
namespace mikehaertl\pdftk;
use mikehaertl\tmp\File;
/**
* Pdf
*
* This class is a wrapper around pdftk.
*
* The class was developed for pdftk 2.x but should also work with older
* versions, but you may have to use slightly different page rotation options
* (e.g 'E' instead 'east').
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @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;
}
}

View file

@ -0,0 +1,233 @@
<?php
namespace mikehaertl\pdftk;
use mikehaertl\tmp\File;
/**
* XfdfFile
*
* This class represents a temporary XFDF file that can be used to fill a PDF
* form with valid unicode characters.
*
* Form data must be passed to the constructor as an array in this form:
*
* ```
* [
* // Field name => 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):
*
* ```
* <field name="Firstname">
* <Value>John</Value>
* </field>
* <field name="Address">
* <field name="Street">
* <Value>Some Street</Value>
* </field>
* <field name="City">
* <Value>Any City</Value>
* </field>
* </field>
* <field name="Pets">
* <Value>Cat</Value>
* <Value>Mouse</Value>
* </field>
* ```
*
* @author Tomas Holy <holy@interconnect.cz>
* @author Michael Härtl <haertl.mike@gmail.com>
* @license http://www.opensource.org/licenses/MIT
*/
class XfdfFile extends File
{
// XFDF file header
const XFDF_HEADER = <<<FDF
<?xml version="1.0" encoding="UTF-8"?>
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
FDF;
// XFDF file footer
const XFDF_FOOTER = <<<FDF
</fields>
</xfdf>
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, "<field name=\"$key\">\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, "<value>$val</value>\n");
}
} else {
// String keys: nested/hierarchical fields
$this->writeFields($fp, $value);
}
fwrite($fp, "</field>\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);
}
}

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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
<?php
use mikehaertl\shellcommand\Command;
// Basic example
$command = new Command('/usr/local/bin/mycommand -a -b');
if ($command->execute()) {
echo $command->getOutput();
} else {
echo $command->getError();
$exitCode = $command->getExitCode();
}
```
### Advanced Features
#### Add Arguments
```php
<?php
$command = new Command('/bin/somecommand');
// Add arguments with correct escaping:
// results in --name='d'\''Artagnan'
$command->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
<?php
$command = new ('jq'); // jq is a pretty printer
$command->setStdIn('{"foo": 0}');
if (!$command->execute()) {
echo $command->getError();
} else {
echo $command->getOutput();
}
// Output:
// {
// "foo": 0
// }
```
From file:
```php
<?php
$fh = fopen('test.json', 'r');
// error checks left out...
$command = new Command('jq');
$command->setStdIn($fh);
if (!$command->execute()) {
echo $command->getError();
} else {
echo $command->getOutput();
}
fclose($fh);
```
From URL:
```php
<?php
$fh = fopen('https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&hourly=temperature_2m,relativehumidity_2m,windspeed_10m', 'r');
// error checks left out...
$command = new Command('jq');
$command->setStdIn($fh);
if (!$command->execute()) {
echo $command->getError();
} else {
echo $command->getOutput();
}
fclose($fh);
```
#### Set Command Instance Options
```php
<?php
// Create command with options array
$command = new Command([
'command' => '/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.

View file

@ -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"
}
}
}

View file

@ -0,0 +1,568 @@
<?php
namespace mikehaertl\shellcommand;
/**
* Command
*
* This class represents a shell command.
*
* Its meant for exuting a single command and capturing stdout and stderr.
*
* Example:
*
* ```
* $command = new Command('/usr/local/bin/mycommand -a -b');
* $command->addArg('--name=', "d'Artagnan");
* if ($command->execute()) {
* echo $command->getOutput();
* } else {
* echo $command->getError();
* $exitCode = $command->getExitCode();
* }
* ```
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @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();
}
}

View file

@ -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

View file

@ -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.

View file

@ -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
<?php
use mikehaertl\tmp\File;
$file = new File('some content', '.html');
// send to client for download
$file->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
<?php
use mikehaertl\tmp\File;
$file = new File('some content', '.html');
$file->delete = false;
```
Default HTTP headers can also be added:
```php
<?php
use mikehaertl\tmp\File;
File::$defaultHeader['X-Header'] = 'My Default';
$file = new File('some content', '.html');
$file->send('home.html');
```

View file

@ -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"
}
}
}

View file

@ -0,0 +1,197 @@
<?php
namespace mikehaertl\tmp;
/**
* File
*
* A convenience class for temporary files.
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @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");
}
}
}
}