added composer setup and the first composer package php-archive

This commit is contained in:
Andreas Gohr 2015-05-15 15:17:39 +02:00
parent 05790a0130
commit 605f8e8d0e
22 changed files with 2538 additions and 18 deletions

7
.gitignore vendored
View File

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

5
composer.json Normal file
View File

@ -0,0 +1,5 @@
{
"require": {
"splitbrain/php-archive": "~1.0"
}
}

65
composer.lock generated Normal file
View File

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

View File

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

View File

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

7
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInita19a915ee98347a0c787119619d2ff9b::getLoader();

413
vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,413 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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 <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
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;
}

9
vendor/composer/autoload_classmap.php vendored Normal file
View File

@ -0,0 +1,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

10
vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'splitbrain\\PHPArchive\\' => array($vendorDir . '/splitbrain/php-archive/src'),
);

50
vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInita19a915ee98347a0c787119619d2ff9b
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInita19a915ee98347a0c787119619d2ff9b', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInita19a915ee98347a0c787119619d2ff9b', 'loadClassLoader'));
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $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;
}

51
vendor/composer/installed.json vendored Normal file
View File

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

View File

@ -0,0 +1,7 @@
*.iml
.idea/
composer.phar
vendor/
composer.lock

View File

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

19
vendor/splitbrain/php-archive/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2015 Andreas Gohr <gohr@cosmocode.de>
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.

61
vendor/splitbrain/php-archive/README.md vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,128 @@
<?php
namespace splitbrain\PHPArchive;
abstract class Archive
{
const COMPRESS_AUTO = -1;
const COMPRESS_NONE = 0;
const COMPRESS_GZIP = 1;
const COMPRESS_BZIP = 2;
/**
* Set the compression level and type
*
* @param int $level Compression level (0 to 9)
* @param int $type Type of compression to use (use COMPRESS_* constants)
* @return mixed
*/
abstract public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO);
/**
* Open an existing archive file for reading
*
* @param string $file
* @throws ArchiveIOException
*/
abstract public function open($file);
/**
* Read the contents of an 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, because rewinding is not possible in bzip2 streams.
* Reopen the file with open() again if you want to do additional operations
*
* @return FileInfo[]
*/
abstract public function contents();
/**
* Extract an existing archive
*
* The $strip parameter allows you to strip a certain number of path components from the filenames
* found in the archive 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 afterwards. 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 array
*/
abstract public function extract($outdir, $strip = '', $exclude = '', $include = '');
/**
* Create a new archive file
*
* If $file is empty, the archive file will be created in memory
*
* @param string $file
*/
abstract public function create($file = '');
/**
* 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
*/
abstract public function addFile($file, $fileinfo = '');
/**
* Add a file to the current 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
*/
abstract public function addData($fileinfo, $data);
/**
* Close the archive, 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
*/
abstract public function close();
/**
* Returns the created in-memory archive data
*
* This implicitly calls close() on the Archive
*/
abstract public function getArchive();
/**
* Save the created in-memory archive data
*
* Note: It is more memory effective to specify the filename in the create() function and
* let the library work on the new file directly.
*
* @param string $file
*/
abstract public function save($file);
}
class ArchiveIOException extends \Exception
{
}
class ArchiveIllegalCompressionException extends \Exception
{
}

View File

@ -0,0 +1,342 @@
<?php
namespace splitbrain\PHPArchive;
/**
* Class FileInfo
*
* stores meta data about a file in an Archive
*
* @author Andreas Gohr <andi@splitbrain.org>
* @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
{
}

View File

@ -0,0 +1,635 @@
<?php
namespace splitbrain\PHPArchive;
/**
* Class Tar
*
* Creates or extracts Tar archives. Supports gz and bzip compression
*
* Long pathnames (>100 chars) are supported in POSIX ustar and GNU longlink formats.
*
* @author Andreas Gohr <andi@splitbrain.org>
* @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;
}
}

View File

@ -0,0 +1,654 @@
<?php
namespace splitbrain\PHPArchive;
/**
* Class Zip
*
* Creates or extracts Zip archives
*
* @author Andreas Gohr <andi@splitbrain.org>
* @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);
}
}