From 605f8e8d0e501057749c50581087ce05089c1af3 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Fri, 15 May 2015 15:17:39 +0200 Subject: added composer setup and the first composer package php-archive --- .gitignore | 7 + composer.json | 5 + composer.lock | 65 +++ inc/init.php | 1 + lib/plugins/extension/helper/extension.php | 27 +- vendor/autoload.php | 7 + vendor/composer/ClassLoader.php | 413 ++++++++++++++++ vendor/composer/autoload_classmap.php | 9 + vendor/composer/autoload_namespaces.php | 9 + vendor/composer/autoload_psr4.php | 10 + vendor/composer/autoload_real.php | 50 ++ vendor/composer/installed.json | 51 ++ vendor/splitbrain/php-archive/.gitignore | 7 + vendor/splitbrain/php-archive/.travis.yml | 13 + vendor/splitbrain/php-archive/LICENSE | 19 + vendor/splitbrain/php-archive/README.md | 61 +++ vendor/splitbrain/php-archive/composer.json | 26 + vendor/splitbrain/php-archive/phpunit.xml | 17 + vendor/splitbrain/php-archive/src/Archive.php | 128 +++++ vendor/splitbrain/php-archive/src/FileInfo.php | 342 +++++++++++++ vendor/splitbrain/php-archive/src/Tar.php | 635 ++++++++++++++++++++++++ vendor/splitbrain/php-archive/src/Zip.php | 654 +++++++++++++++++++++++++ 22 files changed, 2538 insertions(+), 18 deletions(-) create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 vendor/autoload.php create mode 100644 vendor/composer/ClassLoader.php create mode 100644 vendor/composer/autoload_classmap.php create mode 100644 vendor/composer/autoload_namespaces.php create mode 100644 vendor/composer/autoload_psr4.php create mode 100644 vendor/composer/autoload_real.php create mode 100644 vendor/composer/installed.json create mode 100644 vendor/splitbrain/php-archive/.gitignore create mode 100644 vendor/splitbrain/php-archive/.travis.yml create mode 100644 vendor/splitbrain/php-archive/LICENSE create mode 100644 vendor/splitbrain/php-archive/README.md create mode 100644 vendor/splitbrain/php-archive/composer.json create mode 100644 vendor/splitbrain/php-archive/phpunit.xml create mode 100644 vendor/splitbrain/php-archive/src/Archive.php create mode 100644 vendor/splitbrain/php-archive/src/FileInfo.php create mode 100644 vendor/splitbrain/php-archive/src/Tar.php create mode 100644 vendor/splitbrain/php-archive/src/Zip.php diff --git a/.gitignore b/.gitignore index bb39ba7cf..5698e9d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,10 @@ !/lib/plugins/remote.php !/lib/plugins/syntax.php lib/images/*/local/* + +# composer default ignores +vendor/*/*/tests/* +vendor/*/*/test/* +vendor/*/*/doc/* +vendor/*/*/docs/* + diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..14ae94ad7 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "splitbrain/php-archive": "~1.0" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 000000000..753d5c9c0 --- /dev/null +++ b/composer.lock @@ -0,0 +1,65 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "de1f49aad78674b8d56a7f93fb633f65", + "packages": [ + { + "name": "splitbrain/php-archive", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/splitbrain/php-archive.git", + "reference": "a0fbfc2f85ed491f3d2af42cff48a9cb783a8549" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/splitbrain/php-archive/zipball/a0fbfc2f85ed491f3d2af42cff48a9cb783a8549", + "reference": "a0fbfc2f85ed491f3d2af42cff48a9cb783a8549", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.5.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "splitbrain\\PHPArchive\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org" + } + ], + "description": "Pure-PHP implementation to read and write TAR and ZIP archives", + "keywords": [ + "archive", + "extract", + "tar", + "unpack", + "unzip", + "zip" + ], + "time": "2015-02-25 20:15:02" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/inc/init.php b/inc/init.php index bc9ab6d70..6d271dfb0 100644 --- a/inc/init.php +++ b/inc/init.php @@ -191,6 +191,7 @@ global $plugin_controller_class, $plugin_controller; if (empty($plugin_controller_class)) $plugin_controller_class = 'Doku_Plugin_Controller'; // load libraries +require_once(DOKU_INC.'vendor/autoload.php'); require_once(DOKU_INC.'inc/load.php'); // disable gzip if not available diff --git a/lib/plugins/extension/helper/extension.php b/lib/plugins/extension/helper/extension.php index 6c0946b09..d089245b5 100644 --- a/lib/plugins/extension/helper/extension.php +++ b/lib/plugins/extension/helper/extension.php @@ -1035,33 +1035,24 @@ class helper_plugin_extension_extension extends DokuWiki_Plugin { $ext = $this->guess_archive($file); if(in_array($ext, array('tar', 'bz', 'gz'))) { - switch($ext) { - case 'bz': - $compress_type = Tar::COMPRESS_BZIP; - break; - case 'gz': - $compress_type = Tar::COMPRESS_GZIP; - break; - default: - $compress_type = Tar::COMPRESS_NONE; - } - $tar = new Tar(); try { - $tar->open($file, $compress_type); + $tar = new \splitbrain\PHPArchive\Tar(); + $tar->open($file); $tar->extract($target); - } catch (Exception $e) { + } catch (\splitbrain\PHPArchive\ArchiveIOException $e) { throw new Exception($this->getLang('error_decompress').' '.$e->getMessage()); } return true; } elseif($ext == 'zip') { - $zip = new ZipLib(); - $ok = $zip->Extract($file, $target); - - if($ok == -1){ - throw new Exception($this->getLang('error_decompress').' Error extracting the zip archive'); + try { + $zip = new \splitbrain\PHPArchive\Zip(); + $zip->open($file); + $zip->extract($target); + } catch (\splitbrain\PHPArchive\ArchiveIOException $e) { + throw new Exception($this->getLang('error_decompress').' '.$e->getMessage()); } return true; diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 000000000..88c7fd93b --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0 class loader + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * 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 array|string $paths The PSR-0 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + 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; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * 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 array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + 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; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 000000000..7a91153b0 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($vendorDir . '/splitbrain/php-archive/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 000000000..fee79daed --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,50 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + return $loader; + } +} + +function composerRequirea19a915ee98347a0c787119619d2ff9b($file) +{ + require $file; +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 000000000..769865f79 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,51 @@ +[ + { + "name": "splitbrain/php-archive", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/splitbrain/php-archive.git", + "reference": "a0fbfc2f85ed491f3d2af42cff48a9cb783a8549" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/splitbrain/php-archive/zipball/a0fbfc2f85ed491f3d2af42cff48a9cb783a8549", + "reference": "a0fbfc2f85ed491f3d2af42cff48a9cb783a8549", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.5.*" + }, + "time": "2015-02-25 20:15:02", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "splitbrain\\PHPArchive\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org" + } + ], + "description": "Pure-PHP implementation to read and write TAR and ZIP archives", + "keywords": [ + "archive", + "extract", + "tar", + "unpack", + "unzip", + "zip" + ] + } +] diff --git a/vendor/splitbrain/php-archive/.gitignore b/vendor/splitbrain/php-archive/.gitignore new file mode 100644 index 000000000..39b851b56 --- /dev/null +++ b/vendor/splitbrain/php-archive/.gitignore @@ -0,0 +1,7 @@ +*.iml +.idea/ +composer.phar +vendor/ +composer.lock + + diff --git a/vendor/splitbrain/php-archive/.travis.yml b/vendor/splitbrain/php-archive/.travis.yml new file mode 100644 index 000000000..21124ce5d --- /dev/null +++ b/vendor/splitbrain/php-archive/.travis.yml @@ -0,0 +1,13 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - composer self-update + - composer install --prefer-source --no-interaction --dev + +script: phpunit \ No newline at end of file diff --git a/vendor/splitbrain/php-archive/LICENSE b/vendor/splitbrain/php-archive/LICENSE new file mode 100644 index 000000000..66d08e433 --- /dev/null +++ b/vendor/splitbrain/php-archive/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Andreas Gohr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/vendor/splitbrain/php-archive/README.md b/vendor/splitbrain/php-archive/README.md new file mode 100644 index 000000000..4fb673259 --- /dev/null +++ b/vendor/splitbrain/php-archive/README.md @@ -0,0 +1,61 @@ +PHPArchive - Pure PHP ZIP and TAR handling +========================================== + +This library allows to handle new ZIP and TAR archives without the need for any special PHP extensions (gz and bzip are +needed for compression). It can create new files or extract existing ones. + +To keep things simple, the modification (adding or removing files) of existing archives is not supported. + +[![Build Status](https://travis-ci.org/splitbrain/php-archive.svg)](https://travis-ci.org/splitbrain/php-archive) + +Install +------- + +Use composer: + +```php composer.phar require splitbrain/php-archive``` + +Usage +----- + +The usage for the Zip and Tar classes are basically the same. Here are some examples for working with TARs to get +you started. Check the source code comments for more info + +```php +use splitbrain\PHPArchive\Tar; + +// To list the contents of an existing TAR archive, open() it and use contents() on it: +$tar = new Tar(); +$tar->open('myfile.tgz'); +$toc = $tar->contents(); +print_r($toc); // array of FileInfo objects + +// To extract the contents of an existing TAR archive, open() it and use extract() on it: +$tar = new Tar(); +$tar->open('myfile.tgz'); +$tar->extract('/tmp'); + +// To create a new TAR archive directly on the filesystem (low memory requirements), create() it, +$tar = new Tar(); +$tar->create('myfile.tgz'); +$tar->addFile(...); +$tar->addData(...); +... +$tar->close(); + +// To create a TAR archive directly in memory, create() it, add*() files and then either save() +// or getData() it: +$tar = new Tar(); +$tar->create(); +$tar->addFile(...); +$tar->addData(...); +... +$tar->save('myfile.tgz'); // compresses and saves it +echo $tar->getArchive(Archive::COMPRESS_GZIP); // compresses and returns it +``` + +Differences between Tar and Zip: Tars are compressed as a whole while Zips compress each file individually. Therefore +you can call ```setCompression``` before each ```addFile()``` and ```addData()``` functions. + +The FileInfo class can be used to specify additional info like ownership or permissions when adding a file to +an archive. \ No newline at end of file diff --git a/vendor/splitbrain/php-archive/composer.json b/vendor/splitbrain/php-archive/composer.json new file mode 100644 index 000000000..5ad41a8c4 --- /dev/null +++ b/vendor/splitbrain/php-archive/composer.json @@ -0,0 +1,26 @@ +{ + "name": "splitbrain/php-archive", + "description": "Pure-PHP implementation to read and write TAR and ZIP archives", + "keywords": ["zip", "tar", "archive", "unpack", "extract", "unzip"], + "authors": [ + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org" + } + ], + "license": "MIT", + + "require": { + "php": ">=5.3.0" + }, + + "require-dev": { + "phpunit/phpunit": "4.5.*" + }, + + "autoload": { + "psr-4": { + "splitbrain\\PHPArchive\\": "src" + } + } +} diff --git a/vendor/splitbrain/php-archive/phpunit.xml b/vendor/splitbrain/php-archive/phpunit.xml new file mode 100644 index 000000000..d7e1f2428 --- /dev/null +++ b/vendor/splitbrain/php-archive/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./tests/ + + + \ No newline at end of file diff --git a/vendor/splitbrain/php-archive/src/Archive.php b/vendor/splitbrain/php-archive/src/Archive.php new file mode 100644 index 000000000..c60fea777 --- /dev/null +++ b/vendor/splitbrain/php-archive/src/Archive.php @@ -0,0 +1,128 @@ + + * @package splitbrain\PHPArchive + * @license MIT + */ +class FileInfo +{ + + protected $isdir = false; + protected $path = ''; + protected $size = 0; + protected $csize = 0; + protected $mtime = 0; + protected $mode = 0664; + protected $owner = ''; + protected $group = ''; + protected $uid = 0; + protected $gid = 0; + protected $comment = ''; + + /** + * initialize dynamic defaults + * + * @param string $path The path of the file, can also be set later through setPath() + */ + public function __construct($path = '') + { + $this->mtime = time(); + $this->setPath($path); + } + + /** + * Factory to build FileInfo from existing file or directory + * + * @param string $path path to a file on the local file system + * @param string $as optional path to use inside the archive + * @throws FileInfoException + * @return FileInfo + */ + public static function fromPath($path, $as = '') + { + clearstatcache(false, $path); + + if (!file_exists($path)) { + throw new FileInfoException("$path does not exist"); + } + + $stat = stat($path); + $file = new FileInfo(); + + $file->setPath($path); + $file->setIsdir(is_dir($path)); + $file->setMode(fileperms($path)); + $file->setOwner(fileowner($path)); + $file->setGroup(filegroup($path)); + $file->setUid($stat['uid']); + $file->setGid($stat['gid']); + $file->setMtime($stat['mtime']); + + if ($as) { + $file->setPath($as); + } + + return $file; + } + + /** + * @return int + */ + public function getSize() + { + return $this->size; + } + + /** + * @param int $size + */ + public function setSize($size) + { + $this->size = $size; + } + + /** + * @return int + */ + public function getCompressedSize() + { + return $this->csize; + } + + /** + * @param int $csize + */ + public function setCompressedSize($csize) + { + $this->csize = $csize; + } + + /** + * @return int + */ + public function getMtime() + { + return $this->mtime; + } + + /** + * @param int $mtime + */ + public function setMtime($mtime) + { + $this->mtime = $mtime; + } + + /** + * @return int + */ + public function getGid() + { + return $this->gid; + } + + /** + * @param int $gid + */ + public function setGid($gid) + { + $this->gid = $gid; + } + + /** + * @return int + */ + public function getUid() + { + return $this->uid; + } + + /** + * @param int $uid + */ + public function setUid($uid) + { + $this->uid = $uid; + } + + /** + * @return string + */ + public function getComment() + { + return $this->comment; + } + + /** + * @param string $comment + */ + public function setComment($comment) + { + $this->comment = $comment; + } + + /** + * @return string + */ + public function getGroup() + { + return $this->group; + } + + /** + * @param string $group + */ + public function setGroup($group) + { + $this->group = $group; + } + + /** + * @return boolean + */ + public function getIsdir() + { + return $this->isdir; + } + + /** + * @param boolean $isdir + */ + public function setIsdir($isdir) + { + // default mode for directories + if ($isdir && $this->mode === 0664) { + $this->mode = 0775; + } + $this->isdir = $isdir; + } + + /** + * @return int + */ + public function getMode() + { + return $this->mode; + } + + /** + * @param int $mode + */ + public function setMode($mode) + { + $this->mode = $mode; + } + + /** + * @return string + */ + public function getOwner() + { + return $this->owner; + } + + /** + * @param string $owner + */ + public function setOwner($owner) + { + $this->owner = $owner; + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @param string $path + */ + public function setPath($path) + { + $this->path = $this->cleanPath($path); + } + + /** + * Cleans up a path and removes relative parts, also strips leading slashes + * + * @param string $path + * @return string + */ + protected function cleanPath($path) + { + $path = str_replace('\\', '/', $path); + $path = explode('/', $path); + $newpath = array(); + foreach ($path as $p) { + if ($p === '' || $p === '.') { + continue; + } + if ($p === '..') { + array_pop($newpath); + continue; + } + array_push($newpath, $p); + } + return trim(implode('/', $newpath), '/'); + } + + /** + * Strip given prefix or number of path segments from the filename + * + * The $strip parameter allows you to strip a certain number of path components from the filenames + * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when + * an integer is passed as $strip. + * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix, + * the prefix will be stripped. It is recommended to give prefixes with a trailing slash. + * + * @param int|string $strip + * @return FileInfo + */ + public function strip($strip) + { + $filename = $this->getPath(); + $striplen = strlen($strip); + if (is_int($strip)) { + // if $strip is an integer we strip this many path components + $parts = explode('/', $filename); + if (!$this->getIsdir()) { + $base = array_pop($parts); // keep filename itself + } else { + $base = ''; + } + $filename = join('/', array_slice($parts, $strip)); + if ($base) { + $filename .= "/$base"; + } + } else { + // if strip is a string, we strip a prefix here + if (substr($filename, 0, $striplen) == $strip) { + $filename = substr($filename, $striplen); + } + } + + $this->setPath($filename); + } + + /** + * Does the file match the given include and exclude expressions? + * + * Exclude rules take precedence over include rules + * + * @param string $include Regular expression of files to include + * @param string $exclude Regular expression of files to exclude + * @return bool + */ + public function match($include = '', $exclude = '') + { + $extract = true; + if ($include && !preg_match($include, $this->getPath())) { + $extract = false; + } + if ($exclude && preg_match($exclude, $this->getPath())) { + $extract = false; + } + + return $extract; + } +} + +class FileInfoException extends \Exception +{ +} \ No newline at end of file diff --git a/vendor/splitbrain/php-archive/src/Tar.php b/vendor/splitbrain/php-archive/src/Tar.php new file mode 100644 index 000000000..bd78136da --- /dev/null +++ b/vendor/splitbrain/php-archive/src/Tar.php @@ -0,0 +1,635 @@ +100 chars) are supported in POSIX ustar and GNU longlink formats. + * + * @author Andreas Gohr + * @package splitbrain\PHPArchive + * @license MIT + */ +class Tar extends Archive +{ + + protected $file = ''; + protected $comptype = Archive::COMPRESS_AUTO; + protected $complevel = 9; + protected $fh; + protected $memory = ''; + protected $closed = true; + protected $writeaccess = false; + + /** + * Sets the compression to use + * + * @param int $level Compression level (0 to 9) + * @param int $type Type of compression to use (use COMPRESS_* constants) + * @return mixed + */ + public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO) + { + $this->compressioncheck($type); + $this->comptype = $type; + $this->complevel = $level; + } + + /** + * Open an existing TAR file for reading + * + * @param string $file + * @throws ArchiveIOException + */ + public function open($file) + { + $this->file = $file; + + // update compression to mach file + if ($this->comptype == Tar::COMPRESS_AUTO) { + $this->setCompression($this->complevel, $this->filetype($file)); + } + + // open file handles + if ($this->comptype === Archive::COMPRESS_GZIP) { + $this->fh = @gzopen($this->file, 'rb'); + } elseif ($this->comptype === Archive::COMPRESS_BZIP) { + $this->fh = @bzopen($this->file, 'r'); + } else { + $this->fh = @fopen($this->file, 'rb'); + } + + if (!$this->fh) { + throw new ArchiveIOException('Could not open file for reading: '.$this->file); + } + $this->closed = false; + } + + /** + * Read the contents of a TAR archive + * + * This function lists the files stored in the archive + * + * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams. + * Reopen the file with open() again if you want to do additional operations + * + * @throws ArchiveIOException + * @returns FileInfo[] + */ + public function contents() + { + if ($this->closed || !$this->file) { + throw new ArchiveIOException('Can not read from a closed archive'); + } + + $result = array(); + while ($read = $this->readbytes(512)) { + $header = $this->parseHeader($read); + if (!is_array($header)) { + continue; + } + + $this->skipbytes(ceil($header['size'] / 512) * 512); + $result[] = $this->header2fileinfo($header); + } + + $this->close(); + return $result; + } + + /** + * Extract an existing TAR archive + * + * The $strip parameter allows you to strip a certain number of path components from the filenames + * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when + * an integer is passed as $strip. + * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix, + * the prefix will be stripped. It is recommended to give prefixes with a trailing slash. + * + * By default this will extract all files found in the archive. You can restrict the output using the $include + * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If + * $include is set only files that match this expression will be extracted. Files that match the $exclude + * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against + * stripped filenames as described above. + * + * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams. + * Reopen the file with open() again if you want to do additional operations + * + * @param string $outdir the target directory for extracting + * @param int|string $strip either the number of path components or a fixed prefix to strip + * @param string $exclude a regular expression of files to exclude + * @param string $include a regular expression of files to include + * @throws ArchiveIOException + * @return FileInfo[] + */ + public function extract($outdir, $strip = '', $exclude = '', $include = '') + { + if ($this->closed || !$this->file) { + throw new ArchiveIOException('Can not read from a closed archive'); + } + + $outdir = rtrim($outdir, '/'); + @mkdir($outdir, 0777, true); + if (!is_dir($outdir)) { + throw new ArchiveIOException("Could not create directory '$outdir'"); + } + + $extracted = array(); + while ($dat = $this->readbytes(512)) { + // read the file header + $header = $this->parseHeader($dat); + if (!is_array($header)) { + continue; + } + $fileinfo = $this->header2fileinfo($header); + + // apply strip rules + $fileinfo->strip($strip); + + // skip unwanted files + if (!strlen($fileinfo->getPath()) || !$fileinfo->match($include, $exclude)) { + $this->skipbytes(ceil($header['size'] / 512) * 512); + continue; + } + + // create output directory + $output = $outdir.'/'.$fileinfo->getPath(); + $directory = ($fileinfo->getIsdir()) ? $output : dirname($output); + @mkdir($directory, 0777, true); + + // extract data + if (!$fileinfo->getIsdir()) { + $fp = fopen($output, "wb"); + if (!$fp) { + throw new ArchiveIOException('Could not open file for writing: '.$output); + } + + $size = floor($header['size'] / 512); + for ($i = 0; $i < $size; $i++) { + fwrite($fp, $this->readbytes(512), 512); + } + if (($header['size'] % 512) != 0) { + fwrite($fp, $this->readbytes(512), $header['size'] % 512); + } + + fclose($fp); + touch($output, $fileinfo->getMtime()); + chmod($output, $fileinfo->getMode()); + } else { + $this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories + } + + $extracted[] = $fileinfo; + } + + $this->close(); + return $extracted; + } + + /** + * Create a new TAR file + * + * If $file is empty, the tar file will be created in memory + * + * @param string $file + * @throws ArchiveIOException + */ + public function create($file = '') + { + $this->file = $file; + $this->memory = ''; + $this->fh = 0; + + if ($this->file) { + // determine compression + if ($this->comptype == Archive::COMPRESS_AUTO) { + $this->setCompression($this->complevel, $this->filetype($file)); + } + + if ($this->comptype === Archive::COMPRESS_GZIP) { + $this->fh = @gzopen($this->file, 'wb'.$this->complevel); + } elseif ($this->comptype === Archive::COMPRESS_BZIP) { + $this->fh = @bzopen($this->file, 'w'); + } else { + $this->fh = @fopen($this->file, 'wb'); + } + + if (!$this->fh) { + throw new ArchiveIOException('Could not open file for writing: '.$this->file); + } + } + $this->writeaccess = true; + $this->closed = false; + } + + /** + * Add a file to the current TAR archive using an existing file in the filesystem + * + * @param string $file path to the original file + * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original + * @throws ArchiveIOException + */ + public function addFile($file, $fileinfo = '') + { + if (is_string($fileinfo)) { + $fileinfo = FileInfo::fromPath($file, $fileinfo); + } + + if ($this->closed) { + throw new ArchiveIOException('Archive has been closed, files can no longer be added'); + } + + $fp = fopen($file, 'rb'); + if (!$fp) { + throw new ArchiveIOException('Could not open file for reading: '.$file); + } + + // create file header + $this->writeFileHeader($fileinfo); + + // write data + while (!feof($fp)) { + $data = fread($fp, 512); + if ($data === false) { + break; + } + if ($data === '') { + break; + } + $packed = pack("a512", $data); + $this->writebytes($packed); + } + fclose($fp); + } + + /** + * Add a file to the current TAR archive using the given $data as content + * + * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data + * @param string $data binary content of the file to add + * @throws ArchiveIOException + */ + public function addData($fileinfo, $data) + { + if (is_string($fileinfo)) { + $fileinfo = new FileInfo($fileinfo); + } + + if ($this->closed) { + throw new ArchiveIOException('Archive has been closed, files can no longer be added'); + } + + $len = strlen($data); + $fileinfo->setSize($len); + $this->writeFileHeader($fileinfo); + + for ($s = 0; $s < $len; $s += 512) { + $this->writebytes(pack("a512", substr($data, $s, 512))); + } + } + + /** + * Add the closing footer to the archive if in write mode, close all file handles + * + * After a call to this function no more data can be added to the archive, for + * read access no reading is allowed anymore + * + * "Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which + * consists of two 512 blocks of zero bytes" + * + * @link http://www.gnu.org/software/tar/manual/html_chapter/tar_8.html#SEC134 + */ + public function close() + { + if ($this->closed) { + return; + } // we did this already + + // write footer + if ($this->writeaccess) { + $this->writebytes(pack("a512", "")); + $this->writebytes(pack("a512", "")); + } + + // close file handles + if ($this->file) { + if ($this->comptype === Archive::COMPRESS_GZIP) { + gzclose($this->fh); + } elseif ($this->comptype === Archive::COMPRESS_BZIP) { + bzclose($this->fh); + } else { + fclose($this->fh); + } + + $this->file = ''; + $this->fh = 0; + } + + $this->writeaccess = false; + $this->closed = true; + } + + /** + * Returns the created in-memory archive data + * + * This implicitly calls close() on the Archive + */ + public function getArchive() + { + $this->close(); + + if ($this->comptype === Archive::COMPRESS_AUTO) { + $this->comptype = Archive::COMPRESS_NONE; + } + + if ($this->comptype === Archive::COMPRESS_GZIP) { + return gzcompress($this->memory, $this->complevel); + } + if ($this->comptype === Archive::COMPRESS_BZIP) { + return bzcompress($this->memory); + } + return $this->memory; + } + + /** + * Save the created in-memory archive data + * + * Note: It more memory effective to specify the filename in the create() function and + * let the library work on the new file directly. + * + * @param string $file + * @throws ArchiveIOException + */ + public function save($file) + { + if ($this->comptype === Archive::COMPRESS_AUTO) { + $this->setCompression($this->filetype($this->complevel, $file)); + } + + if (!file_put_contents($file, $this->getArchive())) { + throw new ArchiveIOException('Could not write to file: '.$file); + } + } + + /** + * Read from the open file pointer + * + * @param int $length bytes to read + * @return string + */ + protected function readbytes($length) + { + if ($this->comptype === Archive::COMPRESS_GZIP) { + return @gzread($this->fh, $length); + } elseif ($this->comptype === Archive::COMPRESS_BZIP) { + return @bzread($this->fh, $length); + } else { + return @fread($this->fh, $length); + } + } + + /** + * Write to the open filepointer or memory + * + * @param string $data + * @throws ArchiveIOException + * @return int number of bytes written + */ + protected function writebytes($data) + { + if (!$this->file) { + $this->memory .= $data; + $written = strlen($data); + } elseif ($this->comptype === Archive::COMPRESS_GZIP) { + $written = @gzwrite($this->fh, $data); + } elseif ($this->comptype === Archive::COMPRESS_BZIP) { + $written = @bzwrite($this->fh, $data); + } else { + $written = @fwrite($this->fh, $data); + } + if ($written === false) { + throw new ArchiveIOException('Failed to write to archive stream'); + } + return $written; + } + + /** + * Skip forward in the open file pointer + * + * This is basically a wrapper around seek() (and a workaround for bzip2) + * + * @param int $bytes seek to this position + */ + function skipbytes($bytes) + { + if ($this->comptype === Archive::COMPRESS_GZIP) { + @gzseek($this->fh, $bytes, SEEK_CUR); + } elseif ($this->comptype === Archive::COMPRESS_BZIP) { + // there is no seek in bzip2, we simply read on + @bzread($this->fh, $bytes); + } else { + @fseek($this->fh, $bytes, SEEK_CUR); + } + } + + /** + * Write the given file metat data as header + * + * @param FileInfo $fileinfo + */ + protected function writeFileHeader(FileInfo $fileinfo) + { + $this->writeRawFileHeader( + $fileinfo->getPath(), + $fileinfo->getUid(), + $fileinfo->getGid(), + $fileinfo->getMode(), + $fileinfo->getSize(), + $fileinfo->getMtime(), + $fileinfo->getIsdir() ? '5' : '0' + ); + } + + /** + * Write a file header to the stream + * + * @param string $name + * @param int $uid + * @param int $gid + * @param int $perm + * @param int $size + * @param int $mtime + * @param string $typeflag Set to '5' for directories + */ + protected function writeRawFileHeader($name, $uid, $gid, $perm, $size, $mtime, $typeflag = '') + { + // handle filename length restrictions + $prefix = ''; + $namelen = strlen($name); + if ($namelen > 100) { + $file = basename($name); + $dir = dirname($name); + if (strlen($file) > 100 || strlen($dir) > 155) { + // we're still too large, let's use GNU longlink + $this->writeRawFileHeader('././@LongLink', 0, 0, 0, $namelen, 0, 'L'); + for ($s = 0; $s < $namelen; $s += 512) { + $this->writebytes(pack("a512", substr($name, $s, 512))); + } + $name = substr($name, 0, 100); // cut off name + } else { + // we're fine when splitting, use POSIX ustar + $prefix = $dir; + $name = $file; + } + } + + // values are needed in octal + $uid = sprintf("%6s ", decoct($uid)); + $gid = sprintf("%6s ", decoct($gid)); + $perm = sprintf("%6s ", decoct($perm)); + $size = sprintf("%11s ", decoct($size)); + $mtime = sprintf("%11s", decoct($mtime)); + + $data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime); + $data_last = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, ""); + + for ($i = 0, $chks = 0; $i < 148; $i++) { + $chks += ord($data_first[$i]); + } + + for ($i = 156, $chks += 256, $j = 0; $i < 512; $i++, $j++) { + $chks += ord($data_last[$j]); + } + + $this->writebytes($data_first); + + $chks = pack("a8", sprintf("%6s ", decoct($chks))); + $this->writebytes($chks.$data_last); + } + + /** + * Decode the given tar file header + * + * @param string $block a 512 byte block containign the header data + * @return array|bool + */ + protected function parseHeader($block) + { + if (!$block || strlen($block) != 512) { + return false; + } + + for ($i = 0, $chks = 0; $i < 148; $i++) { + $chks += ord($block[$i]); + } + + for ($i = 156, $chks += 256; $i < 512; $i++) { + $chks += ord($block[$i]); + } + + $header = @unpack( + "a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix", + $block + ); + if (!$header) { + return false; + } + + $return['checksum'] = OctDec(trim($header['checksum'])); + if ($return['checksum'] != $chks) { + return false; + } + + $return['filename'] = trim($header['filename']); + $return['perm'] = OctDec(trim($header['perm'])); + $return['uid'] = OctDec(trim($header['uid'])); + $return['gid'] = OctDec(trim($header['gid'])); + $return['size'] = OctDec(trim($header['size'])); + $return['mtime'] = OctDec(trim($header['mtime'])); + $return['typeflag'] = $header['typeflag']; + $return['link'] = trim($header['link']); + $return['uname'] = trim($header['uname']); + $return['gname'] = trim($header['gname']); + + // Handle ustar Posix compliant path prefixes + if (trim($header['prefix'])) { + $return['filename'] = trim($header['prefix']).'/'.$return['filename']; + } + + // Handle Long-Link entries from GNU Tar + if ($return['typeflag'] == 'L') { + // following data block(s) is the filename + $filename = trim($this->readbytes(ceil($header['size'] / 512) * 512)); + // next block is the real header + $block = $this->readbytes(512); + $return = $this->parseHeader($block); + // overwrite the filename + $return['filename'] = $filename; + } + + return $return; + } + + /** + * Creates a FileInfo object from the given parsed header + * + * @param $header + * @return FileInfo + */ + protected function header2fileinfo($header) + { + $fileinfo = new FileInfo(); + $fileinfo->setPath($header['filename']); + $fileinfo->setMode($header['perm']); + $fileinfo->setUid($header['uid']); + $fileinfo->setGid($header['gid']); + $fileinfo->setSize($header['size']); + $fileinfo->setMtime($header['mtime']); + $fileinfo->setOwner($header['uname']); + $fileinfo->setGroup($header['gname']); + $fileinfo->setIsdir((bool) $header['typeflag']); + + return $fileinfo; + } + + /** + * Checks if the given compression type is available and throws an exception if not + * + * @param $comptype + * @throws ArchiveIllegalCompressionException + */ + protected function compressioncheck($comptype) + { + if ($comptype === Archive::COMPRESS_GZIP && !function_exists('gzopen')) { + throw new ArchiveIllegalCompressionException('No gzip support available'); + } + + if ($comptype === Archive::COMPRESS_BZIP && !function_exists('bzopen')) { + throw new ArchiveIllegalCompressionException('No bzip2 support available'); + } + } + + /** + * Guesses the wanted compression from the given filename extension + * + * You don't need to call this yourself. It's used when you pass Archive::COMPRESS_AUTO somewhere + * + * @param string $file + * @return int + */ + public function filetype($file) + { + $file = strtolower($file); + if (substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') { + $comptype = Archive::COMPRESS_GZIP; + } elseif (substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') { + $comptype = Archive::COMPRESS_BZIP; + } else { + $comptype = Archive::COMPRESS_NONE; + } + return $comptype; + } +} diff --git a/vendor/splitbrain/php-archive/src/Zip.php b/vendor/splitbrain/php-archive/src/Zip.php new file mode 100644 index 000000000..c2ff36575 --- /dev/null +++ b/vendor/splitbrain/php-archive/src/Zip.php @@ -0,0 +1,654 @@ + + * @package splitbrain\PHPArchive + * @license MIT + */ +class Zip extends Archive +{ + + protected $file = ''; + protected $fh; + protected $memory = ''; + protected $closed = true; + protected $writeaccess = false; + protected $ctrl_dir; + protected $complevel = 9; + + /** + * Set the compression level. + * + * Compression Type is ignored for ZIP + * + * You can call this function before adding each file to set differen compression levels + * for each file. + * + * @param int $level Compression level (0 to 9) + * @param int $type Type of compression to use ignored for ZIP + * @return mixed + */ + public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO) + { + $this->complevel = $level; + } + + /** + * Open an existing ZIP file for reading + * + * @param string $file + * @throws ArchiveIOException + */ + public function open($file) + { + $this->file = $file; + $this->fh = @fopen($this->file, 'rb'); + if (!$this->fh) { + throw new ArchiveIOException('Could not open file for reading: '.$this->file); + } + $this->closed = false; + } + + /** + * Read the contents of a ZIP archive + * + * This function lists the files stored in the archive, and returns an indexed array of FileInfo objects + * + * The archive is closed afer reading the contents, for API compatibility with TAR files + * Reopen the file with open() again if you want to do additional operations + * + * @throws ArchiveIOException + * @return FileInfo[] + */ + public function contents() + { + if ($this->closed || !$this->file) { + throw new ArchiveIOException('Can not read from a closed archive'); + } + + $result = array(); + + $centd = $this->readCentralDir(); + + @rewind($this->fh); + @fseek($this->fh, $centd['offset']); + + for ($i = 0; $i < $centd['entries']; $i++) { + $result[] = $this->header2fileinfo($this->readCentralFileHeader()); + } + + $this->close(); + return $result; + } + + /** + * Extract an existing ZIP archive + * + * The $strip parameter allows you to strip a certain number of path components from the filenames + * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when + * an integer is passed as $strip. + * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix, + * the prefix will be stripped. It is recommended to give prefixes with a trailing slash. + * + * By default this will extract all files found in the archive. You can restrict the output using the $include + * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If + * $include is set only files that match this expression will be extracted. Files that match the $exclude + * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against + * stripped filenames as described above. + * + * @param string $outdir the target directory for extracting + * @param int|string $strip either the number of path components or a fixed prefix to strip + * @param string $exclude a regular expression of files to exclude + * @param string $include a regular expression of files to include + * @throws ArchiveIOException + * @return FileInfo[] + */ + function extract($outdir, $strip = '', $exclude = '', $include = '') + { + if ($this->closed || !$this->file) { + throw new ArchiveIOException('Can not read from a closed archive'); + } + + $outdir = rtrim($outdir, '/'); + @mkdir($outdir, 0777, true); + + $extracted = array(); + + $cdir = $this->readCentralDir(); + $pos_entry = $cdir['offset']; // begin of the central file directory + + for ($i = 0; $i < $cdir['entries']; $i++) { + // read file header + @fseek($this->fh, $pos_entry); + $header = $this->readCentralFileHeader(); + $header['index'] = $i; + $pos_entry = ftell($this->fh); // position of the next file in central file directory + fseek($this->fh, $header['offset']); // seek to beginning of file header + $header = $this->readFileHeader($header); + $fileinfo = $this->header2fileinfo($header); + + // apply strip rules + $fileinfo->strip($strip); + + // skip unwanted files + if (!strlen($fileinfo->getPath()) || !$fileinfo->match($include, $exclude)) { + continue; + } + + $extracted[] = $fileinfo; + + // create output directory + $output = $outdir.'/'.$fileinfo->getPath(); + $directory = ($header['folder']) ? $output : dirname($output); + @mkdir($directory, 0777, true); + + // nothing more to do for directories + if ($fileinfo->getIsdir()) { + continue; + } + + // compressed files are written to temporary .gz file first + if ($header['compression'] == 0) { + $extractto = $output; + } else { + $extractto = $output.'.gz'; + } + + // open file for writing + $fp = fopen($extractto, "wb"); + if (!$fp) { + throw new ArchiveIOException('Could not open file for writing: '.$extractto); + } + + // prepend compression header + if ($header['compression'] != 0) { + $binary_data = pack( + 'va1a1Va1a1', + 0x8b1f, + chr($header['compression']), + chr(0x00), + time(), + chr(0x00), + chr(3) + ); + fwrite($fp, $binary_data, 10); + } + + // read the file and store it on disk + $size = $header['compressed_size']; + while ($size != 0) { + $read_size = ($size < 2048 ? $size : 2048); + $buffer = fread($this->fh, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + + // finalize compressed file + if ($header['compression'] != 0) { + $binary_data = pack('VV', $header['crc'], $header['size']); + fwrite($fp, $binary_data, 8); + } + + // close file + fclose($fp); + + // unpack compressed file + if ($header['compression'] != 0) { + $gzp = @gzopen($extractto, 'rb'); + if (!$gzp) { + @unlink($extractto); + throw new ArchiveIOException('Failed file extracting. gzip support missing?'); + } + $fp = @fopen($output, 'wb'); + if (!$fp) { + throw new ArchiveIOException('Could not open file for writing: '.$extractto); + } + + $size = $header['size']; + while ($size != 0) { + $read_size = ($size < 2048 ? $size : 2048); + $buffer = gzread($gzp, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + @fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + fclose($fp); + gzclose($gzp); + } + + touch($output, $fileinfo->getMtime()); + //FIXME what about permissions? + } + + $this->close(); + return $extracted; + } + + /** + * Create a new ZIP file + * + * If $file is empty, the zip file will be created in memory + * + * @param string $file + * @throws ArchiveIOException + */ + public function create($file = '') + { + $this->file = $file; + $this->memory = ''; + $this->fh = 0; + + if ($this->file) { + $this->fh = @fopen($this->file, 'wb'); + + if (!$this->fh) { + throw new ArchiveIOException('Could not open file for writing: '.$this->file); + } + } + $this->writeaccess = true; + $this->closed = false; + $this->ctrl_dir = array(); + } + + /** + * Add a file to the current ZIP archive using an existing file in the filesystem + * + * @param string $file path to the original file + * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original + * @throws ArchiveIOException + */ + + /** + * Add a file to the current archive using an existing file in the filesystem + * + * @param string $file path to the original file + * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original + * @throws ArchiveIOException + */ + public function addFile($file, $fileinfo = '') + { + if (is_string($fileinfo)) { + $fileinfo = FileInfo::fromPath($file, $fileinfo); + } + + if ($this->closed) { + throw new ArchiveIOException('Archive has been closed, files can no longer be added'); + } + + $data = @file_get_contents($file); + if ($data === false) { + throw new ArchiveIOException('Could not open file for reading: '.$file); + } + + // FIXME could we stream writing compressed data? gzwrite on a fopen handle? + $this->addData($fileinfo, $data); + } + + /** + * Add a file to the current TAR archive using the given $data as content + * + * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data + * @param string $data binary content of the file to add + * @throws ArchiveIOException + */ + public function addData($fileinfo, $data) + { + if (is_string($fileinfo)) { + $fileinfo = new FileInfo($fileinfo); + } + + if ($this->closed) { + throw new ArchiveIOException('Archive has been closed, files can no longer be added'); + } + + // prepare the various header infos + $dtime = dechex($this->makeDosTime($fileinfo->getMtime())); + $hexdtime = pack( + 'H*', + $dtime[6].$dtime[7]. + $dtime[4].$dtime[5]. + $dtime[2].$dtime[3]. + $dtime[0].$dtime[1] + ); + $size = strlen($data); + $crc = crc32($data); + if ($this->complevel) { + $fmagic = "\x50\x4b\x03\x04\x14\x00\x00\x00\x08\x00"; + $cmagic = "\x50\x4b\x01\x02\x00\x00\x14\x00\x00\x00\x08\x00"; + $data = gzcompress($data, $this->complevel); + $data = substr($data, 2, -4); // strip compression headers + } else { + $fmagic = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00"; + $cmagic = "\x50\x4b\x01\x02\x14\x00\x0a\x00\x00\x00\x00\x00"; + } + $csize = strlen($data); + $offset = $this->dataOffset(); + $name = $fileinfo->getPath(); + + // write data + $this->writebytes($fmagic); + $this->writebytes($hexdtime); + $this->writebytes(pack('V', $crc).pack('V', $csize).pack('V', $size)); //pre header + $this->writebytes(pack('v', strlen($name)).pack('v', 0).$name.$data); //file data + $this->writebytes(pack('V', $crc).pack('V', $csize).pack('V', $size)); //post header + + // add info to central file directory + $cdrec = $cmagic; + $cdrec .= $hexdtime.pack('V', $crc).pack('V', $csize).pack('V', $size); + $cdrec .= pack('v', strlen($name)).pack('v', 0).pack('v', 0); + $cdrec .= pack('v', 0).pack('v', 0).pack('V', 32); + $cdrec .= pack('V', $offset); + $cdrec .= $name; + $this->ctrl_dir[] = $cdrec; + } + + /** + * Add the closing footer to the archive if in write mode, close all file handles + * + * After a call to this function no more data can be added to the archive, for + * read access no reading is allowed anymore + */ + public function close() + { + if ($this->closed) { + return; + } // we did this already + + // write footer + if ($this->writeaccess) { + $offset = $this->dataOffset(); + $ctrldir = join('', $this->ctrl_dir); + $this->writebytes($ctrldir); + $this->writebytes("\x50\x4b\x05\x06\x00\x00\x00\x00"); // EOF CTRL DIR + $this->writebytes(pack('v', count($this->ctrl_dir)).pack('v', count($this->ctrl_dir))); + $this->writebytes(pack('V', strlen($ctrldir)).pack('V', strlen($offset))."\x00\x00"); + $this->ctrl_dir = array(); + } + + // close file handles + if ($this->file) { + fclose($this->fh); + $this->file = ''; + $this->fh = 0; + } + + $this->writeaccess = false; + $this->closed = true; + } + + /** + * Returns the created in-memory archive data + * + * This implicitly calls close() on the Archive + */ + public function getArchive() + { + $this->close(); + + return $this->memory; + } + + /** + * Save the created in-memory archive data + * + * Note: It's more memory effective to specify the filename in the create() function and + * let the library work on the new file directly. + * + * @param $file + * @throws ArchiveIOException + */ + public function save($file) + { + if (!file_put_contents($file, $this->getArchive())) { + throw new ArchiveIOException('Could not write to file: '.$file); + } + } + + /** + * Read the central directory + * + * This key-value list contains general information about the ZIP file + * + * @return array + */ + protected function readCentralDir() + { + $size = filesize($this->file); + if ($size < 277) { + $maximum_size = $size; + } else { + $maximum_size = 277; + } + + @fseek($this->fh, $size - $maximum_size); + $pos = ftell($this->fh); + $bytes = 0x00000000; + + while ($pos < $size) { + $byte = @fread($this->fh, 1); + $bytes = (($bytes << 8) & 0xFFFFFFFF) | ord($byte); + if ($bytes == 0x504b0506) { + break; + } + $pos++; + } + + $data = unpack( + 'vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', + fread($this->fh, 18) + ); + + if ($data['comment_size'] != 0) { + $centd['comment'] = fread($this->fh, $data['comment_size']); + } else { + $centd['comment'] = ''; + } + $centd['entries'] = $data['entries']; + $centd['disk_entries'] = $data['disk_entries']; + $centd['offset'] = $data['offset']; + $centd['disk_start'] = $data['disk_start']; + $centd['size'] = $data['size']; + $centd['disk'] = $data['disk']; + return $centd; + } + + /** + * Read the next central file header + * + * Assumes the current file pointer is pointing at the right position + * + * @return array + */ + protected function readCentralFileHeader() + { + $binary_data = fread($this->fh, 46); + $header = unpack( + 'vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', + $binary_data + ); + + if ($header['filename_len'] != 0) { + $header['filename'] = fread($this->fh, $header['filename_len']); + } else { + $header['filename'] = ''; + } + + if ($header['extra_len'] != 0) { + $header['extra'] = fread($this->fh, $header['extra_len']); + } else { + $header['extra'] = ''; + } + + if ($header['comment_len'] != 0) { + $header['comment'] = fread($this->fh, $header['comment_len']); + } else { + $header['comment'] = ''; + } + + if ($header['mdate'] && $header['mtime']) { + $hour = ($header['mtime'] & 0xF800) >> 11; + $minute = ($header['mtime'] & 0x07E0) >> 5; + $seconde = ($header['mtime'] & 0x001F) * 2; + $year = (($header['mdate'] & 0xFE00) >> 9) + 1980; + $month = ($header['mdate'] & 0x01E0) >> 5; + $day = $header['mdate'] & 0x001F; + $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year); + } else { + $header['mtime'] = time(); + } + + $header['stored_filename'] = $header['filename']; + $header['status'] = 'ok'; + if (substr($header['filename'], -1) == '/') { + $header['external'] = 0x41FF0010; + } + $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0; + + return $header; + } + + /** + * Reads the local file header + * + * This header precedes each individual file inside the zip file. Assumes the current file pointer is pointing at + * the right position already. Enhances this given central header with the data found at the local header. + * + * @param array $header the central file header read previously (see above) + * @return array + */ + function readFileHeader($header) + { + $binary_data = fread($this->fh, 30); + $data = unpack( + 'vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', + $binary_data + ); + + $header['filename'] = fread($this->fh, $data['filename_len']); + if ($data['extra_len'] != 0) { + $header['extra'] = fread($this->fh, $data['extra_len']); + } else { + $header['extra'] = ''; + } + + $header['compression'] = $data['compression']; + foreach (array( + 'size', + 'compressed_size', + 'crc' + ) as $hd) { // On ODT files, these headers are 0. Keep the previous value. + if ($data[$hd] != 0) { + $header[$hd] = $data[$hd]; + } + } + $header['flag'] = $data['flag']; + $header['mdate'] = $data['mdate']; + $header['mtime'] = $data['mtime']; + + if ($header['mdate'] && $header['mtime']) { + $hour = ($header['mtime'] & 0xF800) >> 11; + $minute = ($header['mtime'] & 0x07E0) >> 5; + $seconde = ($header['mtime'] & 0x001F) * 2; + $year = (($header['mdate'] & 0xFE00) >> 9) + 1980; + $month = ($header['mdate'] & 0x01E0) >> 5; + $day = $header['mdate'] & 0x001F; + $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year); + } else { + $header['mtime'] = time(); + } + + $header['stored_filename'] = $header['filename']; + $header['status'] = "ok"; + $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0; + return $header; + } + + /** + * Create fileinfo object from header data + * + * @param $header + * @return FileInfo + */ + protected function header2fileinfo($header) + { + $fileinfo = new FileInfo(); + $fileinfo->setPath($header['filename']); + $fileinfo->setSize($header['size']); + $fileinfo->setCompressedSize($header['compressed_size']); + $fileinfo->setMtime($header['mtime']); + $fileinfo->setComment($header['comment']); + $fileinfo->setIsdir($header['external'] == 0x41FF0010 || $header['external'] == 16); + return $fileinfo; + } + + /** + * Write to the open filepointer or memory + * + * @param string $data + * @throws ArchiveIOException + * @return int number of bytes written + */ + protected function writebytes($data) + { + if (!$this->file) { + $this->memory .= $data; + $written = strlen($data); + } else { + $written = @fwrite($this->fh, $data); + } + if ($written === false) { + throw new ArchiveIOException('Failed to write to archive stream'); + } + return $written; + } + + /** + * Current data pointer position + * + * @fixme might need a -1 + * @return int + */ + protected function dataOffset() + { + if ($this->file) { + return ftell($this->fh); + } else { + return strlen($this->memory); + } + } + + /** + * Create a DOS timestamp from a UNIX timestamp + * + * DOS timestamps start at 1980-01-01, earlier UNIX stamps will be set to this date + * + * @param $time + * @return int + */ + protected function makeDosTime($time) + { + $timearray = getdate($time); + if ($timearray['year'] < 1980) { + $timearray['year'] = 1980; + $timearray['mon'] = 1; + $timearray['mday'] = 1; + $timearray['hours'] = 0; + $timearray['minutes'] = 0; + $timearray['seconds'] = 0; + } + return (($timearray['year'] - 1980) << 25) | + ($timearray['mon'] << 21) | + ($timearray['mday'] << 16) | + ($timearray['hours'] << 11) | + ($timearray['minutes'] << 5) | + ($timearray['seconds'] >> 1); + } + +} -- cgit v1.2.3