diff options
Diffstat (limited to 'wiki/vendor/splitbrain')
-rw-r--r-- | wiki/vendor/splitbrain/php-archive/LICENSE | 19 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-archive/README.md | 70 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-archive/phpunit.xml | 22 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-archive/src/Archive.php | 132 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-archive/src/FileInfo.php | 344 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-archive/src/Tar.php | 667 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-archive/src/Zip.php | 879 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/LICENSE | 21 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/README.md | 158 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/src/CLI.php | 318 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/src/Colors.php | 170 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/src/Exception.php | 35 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/src/Options.php | 470 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/src/PSR3CLI.php | 13 | ||||
-rw-r--r-- | wiki/vendor/splitbrain/php-cli/src/TableFormatter.php | 307 |
15 files changed, 3625 insertions, 0 deletions
diff --git a/wiki/vendor/splitbrain/php-archive/LICENSE b/wiki/vendor/splitbrain/php-archive/LICENSE new file mode 100644 index 0000000..66d08e4 --- /dev/null +++ b/wiki/vendor/splitbrain/php-archive/LICENSE @@ -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. \ No newline at end of file diff --git a/wiki/vendor/splitbrain/php-archive/README.md b/wiki/vendor/splitbrain/php-archive/README.md new file mode 100644 index 0000000..f18764b --- /dev/null +++ b/wiki/vendor/splitbrain/php-archive/README.md @@ -0,0 +1,70 @@ +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. + +[](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 [API docs](https://splitbrain.github.io/php-archive/) for more +info. + + +```php +require_once 'vendor/autoload.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 getArchive() it: +$tar = new Tar(); +$tar->setCompression(9, Archive::COMPRESS_BZIP); +$tar->create(); +$tar->addFile(...); +$tar->addData(...); +... +$tar->save('myfile.tbz'); // compresses and saves it +echo $tar->getArchive(); // 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()``` function call. + +The FileInfo class can be used to specify additional info like ownership or permissions when adding a file to +an archive. diff --git a/wiki/vendor/splitbrain/php-archive/phpunit.xml b/wiki/vendor/splitbrain/php-archive/phpunit.xml new file mode 100644 index 0000000..c5e1ad3 --- /dev/null +++ b/wiki/vendor/splitbrain/php-archive/phpunit.xml @@ -0,0 +1,22 @@ +<?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> + <filter> + <whitelist processUncoveredFilesFromWhitelist="false"> + <directory suffix=".php">src</directory> + </whitelist> + </filter> +</phpunit> diff --git a/wiki/vendor/splitbrain/php-archive/src/Archive.php b/wiki/vendor/splitbrain/php-archive/src/Archive.php new file mode 100644 index 0000000..d672cc6 --- /dev/null +++ b/wiki/vendor/splitbrain/php-archive/src/Archive.php @@ -0,0 +1,132 @@ +<?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 +{ +} + +class ArchiveCorruptedException extends \Exception +{ +} diff --git a/wiki/vendor/splitbrain/php-archive/src/FileInfo.php b/wiki/vendor/splitbrain/php-archive/src/FileInfo.php new file mode 100644 index 0000000..99f60c0 --- /dev/null +++ b/wiki/vendor/splitbrain/php-archive/src/FileInfo.php @@ -0,0 +1,344 @@ +<?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->setSize(filesize($path)); + $file->setUid($stat['uid']); + $file->setGid($stat['gid']); + $file->setMtime($stat['mtime']); + + if ($as) { + $file->setPath($as); + } + + return $file; + } + + /** + * @return int the filesize. always 0 for directories + */ + public function getSize() + { + if($this->isdir) return 0; + 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/wiki/vendor/splitbrain/php-archive/src/Tar.php b/wiki/vendor/splitbrain/php-archive/src/Tar.php new file mode 100644 index 0000000..5f01f39 --- /dev/null +++ b/wiki/vendor/splitbrain/php-archive/src/Tar.php @@ -0,0 +1,667 @@ +<?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; + if($level == 0) $this->comptype = Archive::COMPRESS_NONE; + if($type == Archive::COMPRESS_NONE) $this->complevel = 0; + } + + /** + * 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 ArchiveCorruptedException when the file changes while reading it, the archive will be corrupt and should be deleted + * @throws ArchiveIOException there was trouble reading the given file, it was not added + */ + 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 + $read = 0; + while (!feof($fp)) { + $data = fread($fp, 512); + $read += strlen($data); + if ($data === false) { + break; + } + if ($data === '') { + break; + } + $packed = pack("a512", $data); + $this->writebytes($packed); + } + fclose($fp); + + if($read != $fileinfo->getSize()) { + $this->close(); + throw new ArchiveCorruptedException("The size of $file changed while reading, archive corrupted. read $read expected ".$fileinfo->getSize()); + } + } + + /** + * 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 gzencode($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->complevel, $this->filetype($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 + */ + protected 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 allows to read a max of 8kb at once + while($bytes) { + $toread = min(8192, $bytes); + @bzread($this->fh, $toread); + $bytes -= $toread; + } + } 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 containing the header data + * @return array|false returns false when this was a null block + * @throws ArchiveCorruptedException + */ + protected function parseHeader($block) + { + if (!$block || strlen($block) != 512) { + throw new ArchiveCorruptedException('Unexpected length of header'); + } + + // null byte blocks are ignored + if(trim($block) === '') 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) { + throw new ArchiveCorruptedException('Failed to parse header'); + } + + $return['checksum'] = OctDec(trim($header['checksum'])); + if ($return['checksum'] != $chks) { + throw new ArchiveCorruptedException('Header does not match it\'s checksum'); + } + + $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($return['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 file + * + * Uses magic bytes for existing files, the file extension otherwise + * + * 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) + { + // for existing files, try to read the magic bytes + if(file_exists($file) && is_readable($file) && filesize($file) > 5) { + $fh = @fopen($file, 'rb'); + if(!$fh) return false; + $magic = fread($fh, 5); + fclose($fh); + + if(strpos($magic, "\x42\x5a") === 0) return Archive::COMPRESS_BZIP; + if(strpos($magic, "\x1f\x8b") === 0) return Archive::COMPRESS_GZIP; + } + + // otherwise rely on file name + $file = strtolower($file); + if (substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') { + return Archive::COMPRESS_GZIP; + } elseif (substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') { + return Archive::COMPRESS_BZIP; + } + + return Archive::COMPRESS_NONE; + } +} diff --git a/wiki/vendor/splitbrain/php-archive/src/Zip.php b/wiki/vendor/splitbrain/php-archive/src/Zip.php new file mode 100644 index 0000000..e84516b --- /dev/null +++ b/wiki/vendor/splitbrain/php-archive/src/Zip.php @@ -0,0 +1,879 @@ +<?php + +namespace splitbrain\PHPArchive; + +/** + * Class Zip + * + * Creates or extracts Zip archives + * + * for specs see http://www.pkware.com/appnote + * + * @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[] + */ + 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); + + $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); + unlink($extractto); // remove temporary gz file + } + + 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 use 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 Zip 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 info and compress data + $size = strlen($data); + $crc = crc32($data); + if ($this->complevel) { + $data = gzcompress($data, $this->complevel); + $data = substr($data, 2, -4); // strip compression headers + } + $csize = strlen($data); + $offset = $this->dataOffset(); + $name = $fileinfo->getPath(); + $time = $fileinfo->getMtime(); + + // write local file header + $this->writebytes($this->makeLocalFileHeader( + $time, + $crc, + $size, + $csize, + $name, + (bool) $this->complevel + )); + + // we store no encryption header + + // write data + $this->writebytes($data); + + // we store no data descriptor + + // add info to central file directory + $this->ctrl_dir[] = $this->makeCentralFileRecord( + $offset, + $time, + $crc, + $size, + $csize, + $name, + (bool) $this->complevel + ); + } + + /** + * 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 + + if ($this->writeaccess) { + // write central directory + $offset = $this->dataOffset(); + $ctrldir = join('', $this->ctrl_dir); + $this->writebytes($ctrldir); + + // write end of central directory record + $this->writebytes("\x50\x4b\x05\x06"); // end of central dir signature + $this->writebytes(pack('v', 0)); // number of this disk + $this->writebytes(pack('v', 0)); // number of the disk with the start of the central directory + $this->writebytes(pack('v', + count($this->ctrl_dir))); // total number of entries in the central directory on this disk + $this->writebytes(pack('v', count($this->ctrl_dir))); // total number of entries in the central directory + $this->writebytes(pack('V', strlen($ctrldir))); // size of the central directory + $this->writebytes(pack('V', + $offset)); // offset of start of central directory with respect to the starting disk number + $this->writebytes(pack('v', 0)); // .ZIP file comment length + + $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']); + $header['extradata'] = $this->parseExtra($header['extra']); + } else { + $header['extra'] = ''; + $header['extradata'] = array(); + } + + if ($header['comment_len'] != 0) { + $header['comment'] = fread($this->fh, $header['comment_len']); + } else { + $header['comment'] = ''; + } + + $header['mtime'] = $this->makeUnixTime($header['mdate'], $header['mtime']); + $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 the given central header with the data found at the local header. + * + * @param array $header the central file header read previously (see above) + * @return array + */ + protected 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']); + $header['extradata'] = array_merge($header['extradata'], $this->parseExtra($header['extra'])); + } else { + $header['extra'] = ''; + $header['extradata'] = array(); + } + + $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['mtime'] = $this->makeUnixTime($data['mdate'], $data['mtime']); + + $header['stored_filename'] = $header['filename']; + $header['status'] = "ok"; + $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0; + return $header; + } + + /** + * Parse the extra headers into fields + * + * @param string $header + * @return array + */ + protected function parseExtra($header) + { + $extra = array(); + // parse all extra fields as raw values + while (strlen($header) !== 0) { + $set = unpack('vid/vlen', $header); + $header = substr($header, 4); + $value = substr($header, 0, $set['len']); + $header = substr($header, $set['len']); + $extra[$set['id']] = $value; + } + + // handle known ones + if(isset($extra[0x6375])) { + $extra['utf8comment'] = substr($extra[0x7075], 5); // strip version and crc + } + if(isset($extra[0x7075])) { + $extra['utf8path'] = substr($extra[0x7075], 5); // strip version and crc + } + + return $extra; + } + + /** + * Create fileinfo object from header data + * + * @param $header + * @return FileInfo + */ + protected function header2fileinfo($header) + { + $fileinfo = new FileInfo(); + $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); + + if(isset($header['extradata']['utf8path'])) { + $fileinfo->setPath($header['extradata']['utf8path']); + } else { + $fileinfo->setPath($this->cpToUtf8($header['filename'])); + } + + if(isset($header['extradata']['utf8comment'])) { + $fileinfo->setComment($header['extradata']['utf8comment']); + } else { + $fileinfo->setComment($this->cpToUtf8($header['comment'])); + } + + return $fileinfo; + } + + /** + * Convert the given CP437 encoded string to UTF-8 + * + * Tries iconv with the correct encoding first, falls back to mbstring with CP850 which is + * similar enough. CP437 seems not to be available in mbstring. Lastly falls back to keeping the + * string as is, which is still better than nothing. + * + * On some systems iconv is available, but the codepage is not. We also check for that. + * + * @param $string + * @return string + */ + protected function cpToUtf8($string) + { + if (function_exists('iconv') && @iconv_strlen('', 'CP437') !== false) { + return iconv('CP437', 'UTF-8', $string); + } elseif (function_exists('mb_convert_encoding')) { + return mb_convert_encoding($string, 'UTF-8', 'CP850'); + } else { + return $string; + } + } + + /** + * Convert the given UTF-8 encoded string to CP437 + * + * Same caveats as for cpToUtf8() apply + * + * @param $string + * @return string + */ + protected function utf8ToCp($string) + { + // try iconv first + if (function_exists('iconv')) { + $conv = @iconv('UTF-8', 'CP437//IGNORE', $string); + if($conv) return $conv; // it worked + } + + // still here? iconv failed to convert the string. Try another method + // see http://php.net/manual/en/function.iconv.php#108643 + + if (function_exists('mb_convert_encoding')) { + return mb_convert_encoding($string, 'CP850', 'UTF-8'); + } else { + return $string; + } + } + + + /** + * 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); + } + + /** + * Create a UNIX timestamp from a DOS timestamp + * + * @param $mdate + * @param $mtime + * @return int + */ + protected function makeUnixTime($mdate = null, $mtime = null) + { + if ($mdate && $mtime) { + $year = (($mdate & 0xFE00) >> 9) + 1980; + $month = ($mdate & 0x01E0) >> 5; + $day = $mdate & 0x001F; + + $hour = ($mtime & 0xF800) >> 11; + $minute = ($mtime & 0x07E0) >> 5; + $seconde = ($mtime & 0x001F) << 1; + + $mtime = mktime($hour, $minute, $seconde, $month, $day, $year); + } else { + $mtime = time(); + } + + return $mtime; + } + + /** + * Returns a local file header for the given data + * + * @param int $offset location of the local header + * @param int $ts unix timestamp + * @param int $crc CRC32 checksum of the uncompressed data + * @param int $len length of the uncompressed data + * @param int $clen length of the compressed data + * @param string $name file name + * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen + * @return string + */ + protected function makeCentralFileRecord($offset, $ts, $crc, $len, $clen, $name, $comp = null) + { + if(is_null($comp)) $comp = $len != $clen; + $comp = $comp ? 8 : 0; + $dtime = dechex($this->makeDosTime($ts)); + + list($name, $extra) = $this->encodeFilename($name); + + $header = "\x50\x4b\x01\x02"; // central file header signature + $header .= pack('v', 14); // version made by - VFAT + $header .= pack('v', 20); // version needed to extract - 2.0 + $header .= pack('v', 0); // general purpose flag - no flags set + $header .= pack('v', $comp); // compression method - deflate|none + $header .= pack( + 'H*', + $dtime[6] . $dtime[7] . + $dtime[4] . $dtime[5] . + $dtime[2] . $dtime[3] . + $dtime[0] . $dtime[1] + ); // last mod file time and date + $header .= pack('V', $crc); // crc-32 + $header .= pack('V', $clen); // compressed size + $header .= pack('V', $len); // uncompressed size + $header .= pack('v', strlen($name)); // file name length + $header .= pack('v', strlen($extra)); // extra field length + $header .= pack('v', 0); // file comment length + $header .= pack('v', 0); // disk number start + $header .= pack('v', 0); // internal file attributes + $header .= pack('V', 0); // external file attributes @todo was 0x32!? + $header .= pack('V', $offset); // relative offset of local header + $header .= $name; // file name + $header .= $extra; // extra (utf-8 filename) + + return $header; + } + + /** + * Returns a local file header for the given data + * + * @param int $ts unix timestamp + * @param int $crc CRC32 checksum of the uncompressed data + * @param int $len length of the uncompressed data + * @param int $clen length of the compressed data + * @param string $name file name + * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen + * @return string + */ + protected function makeLocalFileHeader($ts, $crc, $len, $clen, $name, $comp = null) + { + if(is_null($comp)) $comp = $len != $clen; + $comp = $comp ? 8 : 0; + $dtime = dechex($this->makeDosTime($ts)); + + list($name, $extra) = $this->encodeFilename($name); + + $header = "\x50\x4b\x03\x04"; // local file header signature + $header .= pack('v', 20); // version needed to extract - 2.0 + $header .= pack('v', 0); // general purpose flag - no flags set + $header .= pack('v', $comp); // compression method - deflate|none + $header .= pack( + 'H*', + $dtime[6] . $dtime[7] . + $dtime[4] . $dtime[5] . + $dtime[2] . $dtime[3] . + $dtime[0] . $dtime[1] + ); // last mod file time and date + $header .= pack('V', $crc); // crc-32 + $header .= pack('V', $clen); // compressed size + $header .= pack('V', $len); // uncompressed size + $header .= pack('v', strlen($name)); // file name length + $header .= pack('v', strlen($extra)); // extra field length + $header .= $name; // file name + $header .= $extra; // extra (utf-8 filename) + return $header; + } + + /** + * Returns an allowed filename and an extra field header + * + * When encoding stuff outside the 7bit ASCII range it needs to be placed in a separate + * extra field + * + * @param $original + * @return array($filename, $extra) + */ + protected function encodeFilename($original) + { + $cp437 = $this->utf8ToCp($original); + if ($cp437 === $original) { + return array($original, ''); + } + + $extra = pack( + 'vvCV', + 0x7075, // tag + strlen($original) + 5, // length of file + version + crc + 1, // version + crc32($original) // crc + ); + $extra .= $original; + + return array($cp437, $extra); + } +} diff --git a/wiki/vendor/splitbrain/php-cli/LICENSE b/wiki/vendor/splitbrain/php-cli/LICENSE new file mode 100644 index 0000000..4a8abc3 --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 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. diff --git a/wiki/vendor/splitbrain/php-cli/README.md b/wiki/vendor/splitbrain/php-cli/README.md new file mode 100644 index 0000000..7cc52ed --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/README.md @@ -0,0 +1,158 @@ +# PHP-CLI + +PHP-CLI is a simple library that helps with creating nice looking command line scripts. + +It takes care of + +- **option parsing** +- **help page generation** +- **automatic width adjustment** +- **colored output** +- **optional PSR3 compatibility** + +It is lightweight and has **no 3rd party dependencies**. + +[](https://travis-ci.org/splitbrain/php-cli) + +## Installation + +Use composer: + +```php composer.phar require splitbrain/php-cli``` + +## Usage and Examples + +Minimal example: + +```php +#!/usr/bin/php +<?php +require __DIR__ . '/../vendor/autoload.php'; +use splitbrain\phpcli\CLI; +use splitbrain\phpcli\Options; + +class Minimal extends CLI +{ + // register options and arguments + protected function setup(Options $options) + { + $options->setHelp('A very minimal example that does nothing but print a version'); + $options->registerOption('version', 'print version', 'v'); + } + + // implement your code + protected function main(Options $options) + { + if ($options->getOpt('version')) { + $this->info('1.0.0'); + } else { + echo $options->help(); + } + } +} +// execute it +$cli = new Minimal(); +$cli->run(); +``` + + + + +The basic usage is simple: + +- create a class and ``extend splitbrain\phpcli\CLI`` +- implement the ```setup($options)``` method and register options, arguments, commands and set help texts + - ``$options->setHelp()`` adds a general description + - ``$options->registerOption()`` adds an option + - ``$options->registerArgument()`` adds an argument + - ``$options->registerCommand()`` adds a sub command +- implement the ```main($options)``` method and do your business logic there + - ``$options->getOpts`` lets you access set options + - ``$options->getArgs()`` returns the remaining arguments after removing the options + - ``$options->getCmd()`` returns the sub command the user used +- instantiate your class and call ```run()``` on it + +More examples can be found in the examples directory. Please refer to the [API docs](https://splitbrain.github.io/php-cli/) +for further info. + +## Exceptions + +By default the CLI class registers an exception handler and will print the exception's message to the end user and +exit the programm with a non-zero exit code. You can disable this behaviour and catch all exceptions yourself by +passing false to the constructor. + +You can use the provided ``splitbrain\phpcli\Exception`` to signal any problems within your main code yourself. The +exceptions's code will be used as the exit code then. + +Stacktraces will be printed on log level `debug`. + +## Colored output + +Colored output is handled through the ``Colors`` class. It tries to detect if a color terminal is available and only +then uses terminal colors. You can always suppress colored output by passing ``--no-colors`` to your scripts. +Disabling colors will also disable the emoticon prefixes. + +Simple colored log messages can be printed by you using the convinence methods ``success()`` (green), ``info()`` (cyan), +``error()`` (red) or ``fatal()`` (red). The latter will also exit the programm with a non-zero exit code. + +For more complex coloring you can access the color class through ``$this->colors`` in your script. The ``wrap()`` method +is probably what you want to use. + +The table formatter allows coloring full columns. To use that mechanism pass an array of colors as third parameter to +its ``format()`` method. Please note that you can not pass colored texts in the second parameters (text length calculation +and wrapping will fail, breaking your texts). + +## Table Formatter + +The ``TableFormatter`` class allows you to align texts in multiple columns. It tries to figure out the available +terminal width on its own. It can be overwritten by setting a ``COLUMNS`` environment variable. + +The formatter is used through the ``format()`` method which expects at least two arrays: The first defines the column +widths, the second contains the texts to fill into the columns. Between each column a border is printed (a single space +by default). + +See the ``example/table.php`` for sample usage. + +Columns width can be given in three forms: + +- fixed width in characters by providing an integer (eg. ``15``) +- precentages by provifing an integer and a percent sign (eg. ``25%``) +- a single fluid "rest" column marked with an asterisk (eg. ``*``) + +When mixing fixed and percentage widths, percentages refer to the remaining space after all fixed columns have been +assigned. + +Space for borders is automatically calculated. It is recommended to always have some relative (percentage) or a fluid +column to adjust for different terminal widths. + +The table formatter is used for the automatic help screen accessible when calling your script with ``-h`` or ``--help``. + +## PSR-3 Logging + +The CLI class is a fully PSR-3 compatible logger (printing colored log data to STDOUT and STDERR). This is useful when +you call backend code from your CLI that expects a Logger instance to produce any sensible status output while running. + +To use this ability simply inherit from `splitbrain\phpcli\PSR3CLI` instead of `splitbrain\phpcli\CLI`, then pass `$this` +as the logger instance. Be sure you have the suggested `psr/log` composer package installed. + + + +You can adjust the verbosity of your CLI tool using the `--loglevel` parameter. Supported loglevels are the PSR-3 +loglevels and our own `success` level: + +* debug +* info +* notice +* success +* warning +* error +* critical +* alert +* emergency + +Convenience methods for all log levels are available. Placeholder interpolation as described in PSR-3 is available, too. +Messages from `warning` level onwards are printed to `STDERR` all below are printed to `STDOUT`. + +The default log level of your script can be set by overwriting the `$logdefault` member. + +See `example/logging.php` for an example. \ No newline at end of file diff --git a/wiki/vendor/splitbrain/php-cli/src/CLI.php b/wiki/vendor/splitbrain/php-cli/src/CLI.php new file mode 100644 index 0000000..569ec67 --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/src/CLI.php @@ -0,0 +1,318 @@ +<?php + +namespace splitbrain\phpcli; + +/** + * Class CLI + * + * Your commandline script should inherit from this class and implement the abstract methods. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @license MIT + */ +abstract class CLI +{ + /** @var string the executed script itself */ + protected $bin; + /** @var Options the option parser */ + protected $options; + /** @var Colors */ + public $colors; + + /** @var array PSR-3 compatible loglevels and their prefix, color, output channel */ + protected $loglevel = array( + 'debug' => array('', Colors::C_LIGHTGRAY, STDOUT), + 'info' => array('ℹ ', Colors::C_CYAN, STDOUT), + 'notice' => array('☛ ', Colors::C_CYAN, STDOUT), + 'success' => array('✓ ', Colors::C_GREEN, STDOUT), + 'warning' => array('⚠', Colors::C_BROWN, STDERR), + 'error' => array('✗ ', Colors::C_RED, STDERR), + 'critical' => array('☠', Colors::C_LIGHTRED, STDERR), + 'alert' => array('✖ ', Colors::C_LIGHTRED, STDERR), + 'emergency' => array('✘ ', Colors::C_LIGHTRED, STDERR), + ); + + protected $logdefault = 'info'; + + /** + * constructor + * + * Initialize the arguments, set up helper classes and set up the CLI environment + * + * @param bool $autocatch should exceptions be catched and handled automatically? + */ + public function __construct($autocatch = true) + { + if ($autocatch) { + set_exception_handler(array($this, 'fatal')); + } + + $this->colors = new Colors(); + $this->options = new Options($this->colors); + } + + /** + * Register options and arguments on the given $options object + * + * @param Options $options + * @return void + * + * @throws Exception + */ + abstract protected function setup(Options $options); + + /** + * Your main program + * + * Arguments and options have been parsed when this is run + * + * @param Options $options + * @return void + * + * @throws Exception + */ + abstract protected function main(Options $options); + + /** + * Execute the CLI program + * + * Executes the setup() routine, adds default options, initiate the options parsing and argument checking + * and finally executes main() + * + * @throws Exception + */ + public function run() + { + if ('cli' != php_sapi_name()) { + throw new Exception('This has to be run from the command line'); + } + + // setup + $this->setup($this->options); + $this->options->registerOption( + 'help', + 'Display this help screen and exit immeadiately.', + 'h' + ); + $this->options->registerOption( + 'no-colors', + 'Do not use any colors in output. Useful when piping output to other tools or files.' + ); + $this->options->registerOption( + 'loglevel', + 'Minimum level of messages to display. Default is ' . $this->colors->wrap($this->logdefault, Colors::C_CYAN) . '. ' . + 'Valid levels are: debug, info, notice, success, warning, error, critical, alert, emergency.', + null, + 'level' + ); + + // parse + $this->options->parseOptions(); + + // handle defaults + if ($this->options->getOpt('no-colors')) { + $this->colors->disable(); + } + if ($this->options->getOpt('help')) { + echo $this->options->help(); + exit(0); + } + $level = $this->options->getOpt('loglevel', $this->logdefault); + if (!isset($this->loglevel[$level])) $this->fatal('Unknown log level'); + foreach (array_keys($this->loglevel) as $l) { + if ($l == $level) break; + unset($this->loglevel[$l]); + } + + // check arguments + $this->options->checkArguments(); + + // execute + $this->main($this->options); + + exit(0); + } + + // region logging + + /** + * Exits the program on a fatal error + * + * @param \Exception|string $error either an exception or an error message + * @param array $context + */ + public function fatal($error, array $context = array()) + { + $code = 0; + if (is_object($error) && is_a($error, 'Exception')) { + /** @var Exception $error */ + $this->debug(get_class($error) . ' caught in ' . $error->getFile() . ':' . $error->getLine()); + $this->debug($error->getTraceAsString()); + $code = $error->getCode(); + $error = $error->getMessage(); + + } + if (!$code) { + $code = Exception::E_ANY; + } + + $this->critical($error, $context); + exit($code); + } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function emergency($message, array $context = array()) + { + $this->log('emergency', $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + */ + public function alert($message, array $context = array()) + { + $this->log('alert', $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + */ + public function critical($message, array $context = array()) + { + $this->log('critical', $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + */ + public function error($message, array $context = array()) + { + $this->log('error', $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + */ + public function warning($message, array $context = array()) + { + $this->log('warning', $message, $context); + } + + /** + * Normal, positive outcome + * + * @param string $string + * @param array $context + */ + public function success($string, array $context = array()) + { + $this->log('success', $string, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + */ + public function notice($message, array $context = array()) + { + $this->log('notice', $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + */ + public function info($message, array $context = array()) + { + $this->log('info', $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + */ + public function debug($message, array $context = array()) + { + $this->log('debug', $message, $context); + } + + /** + * @param string $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = array()) + { + // is this log level wanted? + if (!isset($this->loglevel[$level])) return; + + /** @var string $prefix */ + /** @var string $color */ + /** @var resource $channel */ + list($prefix, $color, $channel) = $this->loglevel[$level]; + if(!$this->colors->isEnabled()) $prefix = ''; + + $message = $this->interpolate($message, $context); + $this->colors->ptln($prefix . $message, $color, $channel); + } + + /** + * Interpolates context values into the message placeholders. + * + * @param $message + * @param array $context + * @return string + */ + function interpolate($message, array $context = array()) + { + // build a replacement array with braces around the context keys + $replace = array(); + foreach ($context as $key => $val) { + // check that the value can be casted to string + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + + // interpolate replacement values into the message and return + return strtr($message, $replace); + } + + // endregion +} diff --git a/wiki/vendor/splitbrain/php-cli/src/Colors.php b/wiki/vendor/splitbrain/php-cli/src/Colors.php new file mode 100644 index 0000000..ae25256 --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/src/Colors.php @@ -0,0 +1,170 @@ +<?php + +namespace splitbrain\phpcli; + +/** + * Class Colors + * + * Handles color output on (Linux) terminals + * + * @author Andreas Gohr <andi@splitbrain.org> + * @license MIT + */ +class Colors +{ + // these constants make IDE autocompletion easier, but color names can also be passed as strings + const C_RESET = 'reset'; + const C_BLACK = 'black'; + const C_DARKGRAY = 'darkgray'; + const C_BLUE = 'blue'; + const C_LIGHTBLUE = 'lightblue'; + const C_GREEN = 'green'; + const C_LIGHTGREEN = 'lightgreen'; + const C_CYAN = 'cyan'; + const C_LIGHTCYAN = 'lightcyan'; + const C_RED = 'red'; + const C_LIGHTRED = 'lightred'; + const C_PURPLE = 'purple'; + const C_LIGHTPURPLE = 'lightpurple'; + const C_BROWN = 'brown'; + const C_YELLOW = 'yellow'; + const C_LIGHTGRAY = 'lightgray'; + const C_WHITE = 'white'; + + /** @var array known color names */ + protected $colors = array( + self::C_RESET => "\33[0m", + self::C_BLACK => "\33[0;30m", + self::C_DARKGRAY => "\33[1;30m", + self::C_BLUE => "\33[0;34m", + self::C_LIGHTBLUE => "\33[1;34m", + self::C_GREEN => "\33[0;32m", + self::C_LIGHTGREEN => "\33[1;32m", + self::C_CYAN => "\33[0;36m", + self::C_LIGHTCYAN => "\33[1;36m", + self::C_RED => "\33[0;31m", + self::C_LIGHTRED => "\33[1;31m", + self::C_PURPLE => "\33[0;35m", + self::C_LIGHTPURPLE => "\33[1;35m", + self::C_BROWN => "\33[0;33m", + self::C_YELLOW => "\33[1;33m", + self::C_LIGHTGRAY => "\33[0;37m", + self::C_WHITE => "\33[1;37m", + ); + + /** @var bool should colors be used? */ + protected $enabled = true; + + /** + * Constructor + * + * Tries to disable colors for non-terminals + */ + public function __construct() + { + if (function_exists('posix_isatty') && !posix_isatty(STDOUT)) { + $this->enabled = false; + return; + } + if (!getenv('TERM')) { + $this->enabled = false; + return; + } + } + + /** + * enable color output + */ + public function enable() + { + $this->enabled = true; + } + + /** + * disable color output + */ + public function disable() + { + $this->enabled = false; + } + + /** + * @return bool is color support enabled? + */ + public function isEnabled() + { + return $this->enabled; + } + + /** + * Convenience function to print a line in a given color + * + * @param string $line the line to print, a new line is added automatically + * @param string $color one of the available color names + * @param resource $channel file descriptor to write to + * + * @throws Exception + */ + public function ptln($line, $color, $channel = STDOUT) + { + $this->set($color); + fwrite($channel, rtrim($line) . "\n"); + $this->reset(); + } + + /** + * Returns the given text wrapped in the appropriate color and reset code + * + * @param string $text string to wrap + * @param string $color one of the available color names + * @return string the wrapped string + * @throws Exception + */ + public function wrap($text, $color) + { + return $this->getColorCode($color) . $text . $this->getColorCode('reset'); + } + + /** + * Gets the appropriate terminal code for the given color + * + * @param string $color one of the available color names + * @return string color code + * @throws Exception + */ + public function getColorCode($color) + { + if (!$this->enabled) { + return ''; + } + if (!isset($this->colors[$color])) { + throw new Exception("No such color $color"); + } + + return $this->colors[$color]; + } + + /** + * Set the given color for consecutive output + * + * @param string $color one of the supported color names + * @param resource $channel file descriptor to write to + * @throws Exception + */ + public function set($color, $channel = STDOUT) + { + fwrite($channel, $this->getColorCode($color)); + } + + /** + * reset the terminal color + * + * @param resource $channel file descriptor to write to + * + * @throws Exception + */ + public function reset($channel = STDOUT) + { + $this->set('reset', $channel); + } +} diff --git a/wiki/vendor/splitbrain/php-cli/src/Exception.php b/wiki/vendor/splitbrain/php-cli/src/Exception.php new file mode 100644 index 0000000..b2aa981 --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/src/Exception.php @@ -0,0 +1,35 @@ +<?php + +namespace splitbrain\phpcli; + +/** + * Class Exception + * + * The code is used as exit code for the CLI tool. This should probably be extended. Many cases just fall back to the + * E_ANY code. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @license MIT + */ +class Exception extends \Exception +{ + const E_ANY = -1; // no error code specified + const E_UNKNOWN_OPT = 1; //Unrecognized option + const E_OPT_ARG_REQUIRED = 2; //Option requires argument + const E_OPT_ARG_DENIED = 3; //Option not allowed argument + const E_OPT_ABIGUOUS = 4; //Option abiguous + const E_ARG_READ = 5; //Could not read argv + + /** + * @param string $message The Exception message to throw. + * @param int $code The Exception code + * @param \Exception $previous The previous exception used for the exception chaining. + */ + public function __construct($message = "", $code = 0, \Exception $previous = null) + { + if (!$code) { + $code = self::E_ANY; + } + parent::__construct($message, $code, $previous); + } +} diff --git a/wiki/vendor/splitbrain/php-cli/src/Options.php b/wiki/vendor/splitbrain/php-cli/src/Options.php new file mode 100644 index 0000000..4d8b44d --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/src/Options.php @@ -0,0 +1,470 @@ +<?php + +namespace splitbrain\phpcli; + +/** + * Class Options + * + * Parses command line options passed to the CLI script. Allows CLI scripts to easily register all accepted options and + * commands and even generates a help text from this setup. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @license MIT + */ +class Options +{ + /** @var array keeps the list of options to parse */ + protected $setup; + + /** @var array store parsed options */ + protected $options = array(); + + /** @var string current parsed command if any */ + protected $command = ''; + + /** @var array passed non-option arguments */ + protected $args = array(); + + /** @var string the executed script */ + protected $bin; + + /** @var Colors for colored help output */ + protected $colors; + + /** + * Constructor + * + * @param Colors $colors optional configured color object + * @throws Exception when arguments can't be read + */ + public function __construct(Colors $colors = null) + { + if (!is_null($colors)) { + $this->colors = $colors; + } else { + $this->colors = new Colors(); + } + + $this->setup = array( + '' => array( + 'opts' => array(), + 'args' => array(), + 'help' => '' + ) + ); // default command + + $this->args = $this->readPHPArgv(); + $this->bin = basename(array_shift($this->args)); + + $this->options = array(); + } + + /** + * Sets the help text for the tool itself + * + * @param string $help + */ + public function setHelp($help) + { + $this->setup['']['help'] = $help; + } + + /** + * Register the names of arguments for help generation and number checking + * + * This has to be called in the order arguments are expected + * + * @param string $arg argument name (just for help) + * @param string $help help text + * @param bool $required is this a required argument + * @param string $command if theses apply to a sub command only + * @throws Exception + */ + public function registerArgument($arg, $help, $required = true, $command = '') + { + if (!isset($this->setup[$command])) { + throw new Exception("Command $command not registered"); + } + + $this->setup[$command]['args'][] = array( + 'name' => $arg, + 'help' => $help, + 'required' => $required + ); + } + + /** + * This registers a sub command + * + * Sub commands have their own options and use their own function (not main()). + * + * @param string $command + * @param string $help + * @throws Exception + */ + public function registerCommand($command, $help) + { + if (isset($this->setup[$command])) { + throw new Exception("Command $command already registered"); + } + + $this->setup[$command] = array( + 'opts' => array(), + 'args' => array(), + 'help' => $help + ); + + } + + /** + * Register an option for option parsing and help generation + * + * @param string $long multi character option (specified with --) + * @param string $help help text for this option + * @param string|null $short one character option (specified with -) + * @param bool|string $needsarg does this option require an argument? give it a name here + * @param string $command what command does this option apply to + * @throws Exception + */ + public function registerOption($long, $help, $short = null, $needsarg = false, $command = '') + { + if (!isset($this->setup[$command])) { + throw new Exception("Command $command not registered"); + } + + $this->setup[$command]['opts'][$long] = array( + 'needsarg' => $needsarg, + 'help' => $help, + 'short' => $short + ); + + if ($short) { + if (strlen($short) > 1) { + throw new Exception("Short options should be exactly one ASCII character"); + } + + $this->setup[$command]['short'][$short] = $long; + } + } + + /** + * Checks the actual number of arguments against the required number + * + * Throws an exception if arguments are missing. + * + * This is run from CLI automatically and usually does not need to be called directly + * + * @throws Exception + */ + public function checkArguments() + { + $argc = count($this->args); + + $req = 0; + foreach ($this->setup[$this->command]['args'] as $arg) { + if (!$arg['required']) { + break; + } // last required arguments seen + $req++; + } + + if ($req > $argc) { + throw new Exception("Not enough arguments", Exception::E_OPT_ARG_REQUIRED); + } + } + + /** + * Parses the given arguments for known options and command + * + * The given $args array should NOT contain the executed file as first item anymore! The $args + * array is stripped from any options and possible command. All found otions can be accessed via the + * getOpt() function + * + * Note that command options will overwrite any global options with the same name + * + * This is run from CLI automatically and usually does not need to be called directly + * + * @throws Exception + */ + public function parseOptions() + { + $non_opts = array(); + + $argc = count($this->args); + for ($i = 0; $i < $argc; $i++) { + $arg = $this->args[$i]; + + // The special element '--' means explicit end of options. Treat the rest of the arguments as non-options + // and end the loop. + if ($arg == '--') { + $non_opts = array_merge($non_opts, array_slice($this->args, $i + 1)); + break; + } + + // '-' is stdin - a normal argument + if ($arg == '-') { + $non_opts = array_merge($non_opts, array_slice($this->args, $i)); + break; + } + + // first non-option + if ($arg{0} != '-') { + $non_opts = array_merge($non_opts, array_slice($this->args, $i)); + break; + } + + // long option + if (strlen($arg) > 1 && $arg{1} == '-') { + $arg = explode('=', substr($arg, 2), 2); + $opt = array_shift($arg); + $val = array_shift($arg); + + if (!isset($this->setup[$this->command]['opts'][$opt])) { + throw new Exception("No such option '$opt'", Exception::E_UNKNOWN_OPT); + } + + // argument required? + if ($this->setup[$this->command]['opts'][$opt]['needsarg']) { + if (is_null($val) && $i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { + $val = $this->args[++$i]; + } + if (is_null($val)) { + throw new Exception("Option $opt requires an argument", + Exception::E_OPT_ARG_REQUIRED); + } + $this->options[$opt] = $val; + } else { + $this->options[$opt] = true; + } + + continue; + } + + // short option + $opt = substr($arg, 1); + if (!isset($this->setup[$this->command]['short'][$opt])) { + throw new Exception("No such option $arg", Exception::E_UNKNOWN_OPT); + } else { + $opt = $this->setup[$this->command]['short'][$opt]; // store it under long name + } + + // argument required? + if ($this->setup[$this->command]['opts'][$opt]['needsarg']) { + $val = null; + if ($i + 1 < $argc && !preg_match('/^--?[\w]/', $this->args[$i + 1])) { + $val = $this->args[++$i]; + } + if (is_null($val)) { + throw new Exception("Option $arg requires an argument", + Exception::E_OPT_ARG_REQUIRED); + } + $this->options[$opt] = $val; + } else { + $this->options[$opt] = true; + } + } + + // parsing is now done, update args array + $this->args = $non_opts; + + // if not done yet, check if first argument is a command and reexecute argument parsing if it is + if (!$this->command && $this->args && isset($this->setup[$this->args[0]])) { + // it is a command! + $this->command = array_shift($this->args); + $this->parseOptions(); // second pass + } + } + + /** + * Get the value of the given option + * + * Please note that all options are accessed by their long option names regardless of how they were + * specified on commandline. + * + * Can only be used after parseOptions() has been run + * + * @param mixed $option + * @param bool|string $default what to return if the option was not set + * @return bool|string|string[] + */ + public function getOpt($option = null, $default = false) + { + if ($option === null) { + return $this->options; + } + + if (isset($this->options[$option])) { + return $this->options[$option]; + } + return $default; + } + + /** + * Return the found command if any + * + * @return string + */ + public function getCmd() + { + return $this->command; + } + + /** + * Get all the arguments passed to the script + * + * This will not contain any recognized options or the script name itself + * + * @return array + */ + public function getArgs() + { + return $this->args; + } + + /** + * Builds a help screen from the available options. You may want to call it from -h or on error + * + * @return string + * + * @throws Exception + */ + public function help() + { + $tf = new TableFormatter($this->colors); + $text = ''; + + $hascommands = (count($this->setup) > 1); + foreach ($this->setup as $command => $config) { + $hasopts = (bool)$this->setup[$command]['opts']; + $hasargs = (bool)$this->setup[$command]['args']; + + // usage or command syntax line + if (!$command) { + $text .= $this->colors->wrap('USAGE:', Colors::C_BROWN); + $text .= "\n"; + $text .= ' ' . $this->bin; + $mv = 2; + } else { + $text .= "\n"; + $text .= $this->colors->wrap(' ' . $command, Colors::C_PURPLE); + $mv = 4; + } + + if ($hasopts) { + $text .= ' ' . $this->colors->wrap('<OPTIONS>', Colors::C_GREEN); + } + + if (!$command && $hascommands) { + $text .= ' ' . $this->colors->wrap('<COMMAND> ...', Colors::C_PURPLE); + } + + foreach ($this->setup[$command]['args'] as $arg) { + $out = $this->colors->wrap('<' . $arg['name'] . '>', Colors::C_CYAN); + + if (!$arg['required']) { + $out = '[' . $out . ']'; + } + $text .= ' ' . $out; + } + $text .= "\n"; + + // usage or command intro + if ($this->setup[$command]['help']) { + $text .= "\n"; + $text .= $tf->format( + array($mv, '*'), + array('', $this->setup[$command]['help'] . "\n") + ); + } + + // option description + if ($hasopts) { + if (!$command) { + $text .= "\n"; + $text .= $this->colors->wrap('OPTIONS:', Colors::C_BROWN); + } + $text .= "\n"; + foreach ($this->setup[$command]['opts'] as $long => $opt) { + + $name = ''; + if ($opt['short']) { + $name .= '-' . $opt['short']; + if ($opt['needsarg']) { + $name .= ' <' . $opt['needsarg'] . '>'; + } + $name .= ', '; + } + $name .= "--$long"; + if ($opt['needsarg']) { + $name .= ' <' . $opt['needsarg'] . '>'; + } + + $text .= $tf->format( + array($mv, '30%', '*'), + array('', $name, $opt['help']), + array('', 'green', '') + ); + $text .= "\n"; + } + } + + // argument description + if ($hasargs) { + if (!$command) { + $text .= "\n"; + $text .= $this->colors->wrap('ARGUMENTS:', Colors::C_BROWN); + } + $text .= "\n"; + foreach ($this->setup[$command]['args'] as $arg) { + $name = '<' . $arg['name'] . '>'; + + $text .= $tf->format( + array($mv, '30%', '*'), + array('', $name, $arg['help']), + array('', 'cyan', '') + ); + } + } + + // head line and intro for following command documentation + if (!$command && $hascommands) { + $text .= "\n"; + $text .= $this->colors->wrap('COMMANDS:', Colors::C_BROWN); + $text .= "\n"; + $text .= $tf->format( + array($mv, '*'), + array('', 'This tool accepts a command as first parameter as outlined below:') + ); + $text .= "\n"; + } + } + + return $text; + } + + /** + * Safely read the $argv PHP array across different PHP configurations. + * Will take care on register_globals and register_argc_argv ini directives + * + * @throws Exception + * @return array the $argv PHP array or PEAR error if not registered + */ + private function readPHPArgv() + { + global $argv; + if (!is_array($argv)) { + if (!@is_array($_SERVER['argv'])) { + if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { + throw new Exception( + "Could not read cmd args (register_argc_argv=Off?)", + Exception::E_ARG_READ + ); + } + return $GLOBALS['HTTP_SERVER_VARS']['argv']; + } + return $_SERVER['argv']; + } + return $argv; + } +} + diff --git a/wiki/vendor/splitbrain/php-cli/src/PSR3CLI.php b/wiki/vendor/splitbrain/php-cli/src/PSR3CLI.php new file mode 100644 index 0000000..ef744f3 --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/src/PSR3CLI.php @@ -0,0 +1,13 @@ +<?php + +namespace splitbrain\phpcli; + +use Psr\Log\LoggerInterface; + +/** + * Class PSR3CLI + * + * The same as CLI, but implements the PSR-3 logger interface + */ +abstract class PSR3CLI extends CLI implements LoggerInterface { +} \ No newline at end of file diff --git a/wiki/vendor/splitbrain/php-cli/src/TableFormatter.php b/wiki/vendor/splitbrain/php-cli/src/TableFormatter.php new file mode 100644 index 0000000..73fd6b3 --- /dev/null +++ b/wiki/vendor/splitbrain/php-cli/src/TableFormatter.php @@ -0,0 +1,307 @@ +<?php + +namespace splitbrain\phpcli; + +/** + * Class TableFormatter + * + * Output text in multiple columns + * + * @author Andreas Gohr <andi@splitbrain.org> + * @license MIT + */ +class TableFormatter +{ + /** @var string border between columns */ + protected $border = ' '; + + /** @var int the terminal width */ + protected $max = 74; + + /** @var Colors for coloring output */ + protected $colors; + + /** + * TableFormatter constructor. + * + * @param Colors|null $colors + */ + public function __construct(Colors $colors = null) + { + // try to get terminal width + $width = 0; + if (isset($_SERVER['COLUMNS'])) { + // from environment + $width = (int)$_SERVER['COLUMNS']; + } + if (!$width) { + // via tput command + $width = @exec('tput cols'); + } + if ($width) { + $this->max = $width - 1; + } + + if ($colors) { + $this->colors = $colors; + } else { + $this->colors = new Colors(); + } + } + + /** + * The currently set border (defaults to ' ') + * + * @return string + */ + public function getBorder() + { + return $this->border; + } + + /** + * Set the border. The border is set between each column. Its width is + * added to the column widths. + * + * @param string $border + */ + public function setBorder($border) + { + $this->border = $border; + } + + /** + * Width of the terminal in characters + * + * initially autodetected + * + * @return int + */ + public function getMaxWidth() + { + return $this->max; + } + + /** + * Set the width of the terminal to assume (in characters) + * + * @param int $max + */ + public function setMaxWidth($max) + { + $this->max = $max; + } + + /** + * Takes an array with dynamic column width and calculates the correct width + * + * Column width can be given as fixed char widths, percentages and a single * width can be given + * for taking the remaining available space. When mixing percentages and fixed widths, percentages + * refer to the remaining space after allocating the fixed width + * + * @param array $columns + * @return int[] + * @throws Exception + */ + protected function calculateColLengths($columns) + { + $idx = 0; + $border = $this->strlen($this->border); + $fixed = (count($columns) - 1) * $border; // borders are used already + $fluid = -1; + + // first pass for format check and fixed columns + foreach ($columns as $idx => $col) { + // handle fixed columns + if ((string)intval($col) === (string)$col) { + $fixed += $col; + continue; + } + // check if other colums are using proper units + if (substr($col, -1) == '%') { + continue; + } + if ($col == '*') { + // only one fluid + if ($fluid < 0) { + $fluid = $idx; + continue; + } else { + throw new Exception('Only one fluid column allowed!'); + } + } + throw new Exception("unknown column format $col"); + } + + $alloc = $fixed; + $remain = $this->max - $alloc; + + // second pass to handle percentages + foreach ($columns as $idx => $col) { + if (substr($col, -1) != '%') { + continue; + } + $perc = floatval($col); + + $real = (int)floor(($perc * $remain) / 100); + + $columns[$idx] = $real; + $alloc += $real; + } + + $remain = $this->max - $alloc; + if ($remain < 0) { + throw new Exception("Wanted column widths exceed available space"); + } + + // assign remaining space + if ($fluid < 0) { + $columns[$idx] += ($remain); // add to last column + } else { + $columns[$fluid] = $remain; + } + + return $columns; + } + + /** + * Displays text in multiple word wrapped columns + * + * @param int[] $columns list of column widths (in characters, percent or '*') + * @param string[] $texts list of texts for each column + * @param array $colors A list of color names to use for each column. use empty string for default + * @return string + * @throws Exception + */ + public function format($columns, $texts, $colors = array()) + { + $columns = $this->calculateColLengths($columns); + + $wrapped = array(); + $maxlen = 0; + + foreach ($columns as $col => $width) { + $wrapped[$col] = explode("\n", $this->wordwrap($texts[$col], $width, "\n", true)); + $len = count($wrapped[$col]); + if ($len > $maxlen) { + $maxlen = $len; + } + + } + + $last = count($columns) - 1; + $out = ''; + for ($i = 0; $i < $maxlen; $i++) { + foreach ($columns as $col => $width) { + if (isset($wrapped[$col][$i])) { + $val = $wrapped[$col][$i]; + } else { + $val = ''; + } + $chunk = $this->pad($val, $width); + if (isset($colors[$col]) && $colors[$col]) { + $chunk = $this->colors->wrap($chunk, $colors[$col]); + } + $out .= $chunk; + + // border + if ($col != $last) { + $out .= $this->border; + } + } + $out .= "\n"; + } + return $out; + + } + + /** + * Pad the given string to the correct length + * + * @param string $string + * @param int $len + * @return string + */ + protected function pad($string, $len) + { + $strlen = $this->strlen($string); + if ($strlen > $len) return $string; + + $pad = $len - $strlen; + return $string . str_pad('', $pad, ' '); + } + + /** + * Measures char length in UTF-8 when possible + * + * @param $string + * @return int + */ + protected function strlen($string) + { + // don't count color codes + $string = preg_replace("/\33\\[\\d+(;\\d+)?m/", '', $string); + + if (function_exists('mb_strlen')) { + return mb_strlen($string, 'utf-8'); + } + + return strlen($string); + } + + /** + * @param string $string + * @param int $start + * @param int|null $length + * @return string + */ + protected function substr($string, $start = 0, $length = null) + { + if (function_exists('mb_substr')) { + return mb_substr($string, $start, $length); + } else { + return substr($string, $start, $length); + } + } + + /** + * @param string $str + * @param int $width + * @param string $break + * @param bool $cut + * @return string + * @link http://stackoverflow.com/a/4988494 + */ + protected function wordwrap($str, $width = 75, $break = "\n", $cut = false) + { + $lines = explode($break, $str); + foreach ($lines as &$line) { + $line = rtrim($line); + if ($this->strlen($line) <= $width) { + continue; + } + $words = explode(' ', $line); + $line = ''; + $actual = ''; + foreach ($words as $word) { + if ($this->strlen($actual . $word) <= $width) { + $actual .= $word . ' '; + } else { + if ($actual != '') { + $line .= rtrim($actual) . $break; + } + $actual = $word; + if ($cut) { + while ($this->strlen($actual) > $width) { + $line .= $this->substr($actual, 0, $width) . $break; + $actual = $this->substr($actual, $width); + } + } + $actual .= ' '; + } + } + $line .= trim($actual); + } + return implode($break, $lines); + } +} \ No newline at end of file |