mirror of
https://github.com/Poniverse/Pony.fm.git
synced 2024-11-23 13:37:59 +01:00
263ea48c5b
Shift automatically applies the Laravel coding style - which uses the PSR-2 coding style as a base with some minor additions. You may customize the adopted coding style by adding your own [PHP CS Fixer][1] `.php_cs` config file to your project root. Feel free to use [Shift's Laravel ruleset][2] to help you get started. [1]: https://github.com/FriendsOfPHP/PHP-CS-Fixer [2]: https://gist.github.com/laravel-shift/cab527923ed2a109dda047b97d53c200
598 lines
22 KiB
PHP
598 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* Class to create zip file, aimed at large files, or even large target zip file.
|
|
* This class will stream the generated zip file directly to the HTTP client as the content is added.
|
|
*
|
|
* If you need the Zip file data on the server, for storage in a database of the server file system, look at
|
|
* the Zip class at http://www.phpclasses.org/browse/package/6110.html
|
|
*
|
|
* License: GNU LGPL, Attribution required for commercial implementations, requested for everything else.
|
|
*
|
|
* Inspired on CreateZipFile by Rochak Chauhan www.rochakchauhan.com (http://www.phpclasses.org/browse/package/2322.html)
|
|
* and
|
|
* http://www.pkware.com/documents/casestudies/APPNOTE.TXT Zip file specification.
|
|
*
|
|
* @author A. Grandt <php@grandt.com>
|
|
* @copyright 2009-2013 A. Grandt
|
|
* @license GNU LGPL, Attribution required for commercial implementations, requested for everything else.
|
|
* @link http://www.phpclasses.org/package/6116
|
|
* @link https://github.com/Grandt/PHPZip
|
|
* @version 1.37
|
|
*/
|
|
class ZipStream
|
|
{
|
|
const VERSION = 1.37;
|
|
|
|
const ZIP_LOCAL_FILE_HEADER = "\x50\x4b\x03\x04"; // Local file header signature
|
|
const ZIP_CENTRAL_FILE_HEADER = "\x50\x4b\x01\x02"; // Central file header signature
|
|
const ZIP_END_OF_CENTRAL_DIRECTORY = "\x50\x4b\x05\x06\x00\x00\x00\x00"; //end of Central directory record
|
|
|
|
const EXT_FILE_ATTR_DIR = "\x10\x00\xFF\x41";
|
|
const EXT_FILE_ATTR_FILE = "\x00\x00\xFF\x81";
|
|
|
|
const ATTR_VERSION_TO_EXTRACT = "\x14\x00"; // Version needed to extract
|
|
const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version
|
|
|
|
private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB)
|
|
|
|
private $zipComment = null;
|
|
private $cdRec = []; // central directory
|
|
private $offset = 0;
|
|
private $isFinalized = false;
|
|
private $addExtraField = true;
|
|
|
|
private $streamChunkSize = 16384; // 65536;
|
|
private $streamFilePath = null;
|
|
private $streamTimeStamp = null;
|
|
private $streamComment = null;
|
|
private $streamFile = null;
|
|
private $streamData = null;
|
|
private $streamFileLength = 0;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param string $archiveName Name to send to the HTTP client.
|
|
* @param string $contentType Content mime type. Optional, defaults to "application/zip".
|
|
*/
|
|
public function __construct($archiveName = '', $contentType = 'application/zip')
|
|
{
|
|
if (! function_exists('sys_get_temp_dir')) {
|
|
die('ERROR: ZipStream '.self::VERSION.' requires PHP version 5.2.1 or above.');
|
|
}
|
|
|
|
$headerFile = null;
|
|
$headerLine = null;
|
|
if (! headers_sent($headerFile, $headerLine) or die("<p><strong>Error:</strong> Unable to send file $archiveName. HTML Headers have already been sent from <strong>$headerFile</strong> in line <strong>$headerLine</strong></p>")) {
|
|
if ((ob_get_contents() === false || ob_get_contents() == '') or die("\n<p><strong>Error:</strong> Unable to send file <strong>$archiveName.epub</strong>. Output buffer contains the following text (typically warnings or errors):<br>".ob_get_contents().'</p>')) {
|
|
if (ini_get('zlib.output_compression')) {
|
|
ini_set('zlib.output_compression', 'Off');
|
|
}
|
|
|
|
header('Pragma: public');
|
|
header('Last-Modified: '.gmdate('D, d M Y H:i:s T'));
|
|
header('Expires: 0');
|
|
header('Accept-Ranges: bytes');
|
|
//header("Connection: Keep-Alive");
|
|
header('Content-Type: '.$contentType);
|
|
header('Content-Disposition: attachment; filename="'.$archiveName.'";');
|
|
header('Content-Transfer-Encoding: binary');
|
|
flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
public function __destruct()
|
|
{
|
|
$this->isFinalized = true;
|
|
$this->cdRec = null;
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Extra fields on the Zip directory records are Unix time codes needed for compatibility on the default Mac zip archive tool.
|
|
* These are enabled as default, as they do no harm elsewhere and only add 26 bytes per file added.
|
|
*
|
|
* @param bool $setExtraField TRUE (default) will enable adding of extra fields, anything else will disable it.
|
|
*/
|
|
public function setExtraField($setExtraField = true)
|
|
{
|
|
$this->addExtraField = ($setExtraField === true);
|
|
}
|
|
|
|
/**
|
|
* Set Zip archive comment.
|
|
*
|
|
* @param string $newComment New comment. null to clear.
|
|
* @return bool $success
|
|
*/
|
|
public function setComment($newComment = null)
|
|
{
|
|
if ($this->isFinalized) {
|
|
return false;
|
|
}
|
|
$this->zipComment = $newComment;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add an empty directory entry to the zip archive.
|
|
* Basically this is only used if an empty directory is added.
|
|
*
|
|
* @param string $directoryPath Directory Path and name to be added to the archive.
|
|
* @param int $timestamp (Optional) Timestamp for the added directory, if omitted or set to 0, the current time will be used.
|
|
* @param string $fileComment (Optional) Comment to be added to the archive for this directory. To use fileComment, timestamp must be given.
|
|
* @return bool $success
|
|
*/
|
|
public function addDirectory($directoryPath, $timestamp = 0, $fileComment = null)
|
|
{
|
|
if ($this->isFinalized) {
|
|
return false;
|
|
}
|
|
|
|
$directoryPath = str_replace('\\', '/', $directoryPath);
|
|
$directoryPath = rtrim($directoryPath, '/');
|
|
|
|
if (strlen($directoryPath) > 0) {
|
|
$this->buildZipEntry($directoryPath.'/', $fileComment, "\x00\x00", "\x00\x00", $timestamp, "\x00\x00\x00\x00", 0, 0, self::EXT_FILE_ATTR_DIR);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add a file to the archive at the specified location and file name.
|
|
*
|
|
* @param string $data File data.
|
|
* @param string $filePath Filepath and name to be used in the archive.
|
|
* @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
|
|
* @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
|
|
* @return bool $success
|
|
*/
|
|
public function addFile($data, $filePath, $timestamp = 0, $fileComment = null)
|
|
{
|
|
if ($this->isFinalized) {
|
|
return false;
|
|
}
|
|
|
|
if (is_resource($data) && get_resource_type($data) == 'stream') {
|
|
$this->addLargeFile($data, $filePath, $timestamp, $fileComment);
|
|
|
|
return false;
|
|
}
|
|
|
|
$gzType = "\x08\x00"; // Compression type 8 = deflate
|
|
$gpFlags = "\x00\x00"; // General Purpose bit flags for compression type 8 it is: 0=Normal, 1=Maximum, 2=Fast, 3=super fast compression.
|
|
$dataLength = strlen($data);
|
|
$fileCRC32 = pack('V', crc32($data));
|
|
|
|
$gzData = gzcompress($data);
|
|
$gzData = substr(substr($gzData, 0, strlen($gzData) - 4), 2); // gzcompress adds a 2 byte header and 4 byte CRC we can't use.
|
|
// The 2 byte header does contain useful data, though in this case the 2 parameters we'd be interrested in will always be 8 for compression type, and 2 for General purpose flag.
|
|
$gzLength = strlen($gzData);
|
|
|
|
if ($gzLength >= $dataLength) {
|
|
$gzLength = $dataLength;
|
|
$gzData = $data;
|
|
$gzType = "\x00\x00"; // Compression type 0 = stored
|
|
$gpFlags = "\x00\x00"; // Compression type 0 = stored
|
|
}
|
|
|
|
$this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, self::EXT_FILE_ATTR_FILE);
|
|
|
|
echo $gzData;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add the content to a directory.
|
|
*
|
|
* @author Adam Schmalhofer <Adam.Schmalhofer@gmx.de>
|
|
* @author A. Grandt
|
|
*
|
|
* @param string $realPath Path on the file system.
|
|
* @param string $zipPath Filepath and name to be used in the archive.
|
|
* @param bool $recursive Add content recursively, default is TRUE.
|
|
* @param bool $followSymlinks Follow and add symbolic links, if they are accessible, default is TRUE.
|
|
* @param array &$addedFiles Reference to the added files, this is used to prevent duplicates, efault is an empty array.
|
|
* If you start the function by parsing an array, the array will be populated with the realPath
|
|
* and zipPath kay/value pairs added to the archive by the function.
|
|
*/
|
|
public function addDirectoryContent($realPath, $zipPath, $recursive = true, $followSymlinks = true, &$addedFiles = [])
|
|
{
|
|
if (file_exists($realPath) && ! isset($addedFiles[realpath($realPath)])) {
|
|
if (is_dir($realPath)) {
|
|
$this->addDirectory($zipPath);
|
|
}
|
|
|
|
$addedFiles[realpath($realPath)] = $zipPath;
|
|
|
|
$iter = new DirectoryIterator($realPath);
|
|
foreach ($iter as $file) {
|
|
if ($file->isDot()) {
|
|
continue;
|
|
}
|
|
$newRealPath = $file->getPathname();
|
|
$newZipPath = self::pathJoin($zipPath, $file->getFilename());
|
|
|
|
if (file_exists($newRealPath) && ($followSymlinks === true || ! is_link($newRealPath))) {
|
|
if ($file->isFile()) {
|
|
$addedFiles[realpath($newRealPath)] = $newZipPath;
|
|
$this->addLargeFile($newRealPath, $newZipPath);
|
|
} elseif ($recursive === true) {
|
|
$this->addDirectoryContent($newRealPath, $newZipPath, $recursive);
|
|
} else {
|
|
$this->addDirectory($zipPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a file to the archive at the specified location and file name.
|
|
*
|
|
* @param string $dataFile File name/path.
|
|
* @param string $filePath Filepath and name to be used in the archive.
|
|
* @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
|
|
* @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
|
|
* @return bool $success
|
|
*/
|
|
public function addLargeFile($dataFile, $filePath, $timestamp = 0, $fileComment = null)
|
|
{
|
|
if ($this->isFinalized) {
|
|
return false;
|
|
}
|
|
|
|
if (is_string($dataFile) && is_file($dataFile)) {
|
|
$this->processFile($dataFile, $filePath, $timestamp, $fileComment);
|
|
} elseif (is_resource($dataFile) && get_resource_type($dataFile) == 'stream') {
|
|
$fh = $dataFile;
|
|
$this->openStream($filePath, $timestamp, $fileComment);
|
|
|
|
while (! feof($fh)) {
|
|
$this->addStreamData(fread($fh, $this->streamChunkSize));
|
|
}
|
|
$this->closeStream($this->addExtraField);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create a stream to be used for large entries.
|
|
*
|
|
* @param string $filePath Filepath and name to be used in the archive.
|
|
* @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
|
|
* @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
|
|
* @return bool $success
|
|
*/
|
|
public function openStream($filePath, $timestamp = 0, $fileComment = null)
|
|
{
|
|
if (! function_exists('sys_get_temp_dir')) {
|
|
die('ERROR: Zip '.self::VERSION.' requires PHP version 5.2.1 or above if large files are used.');
|
|
}
|
|
|
|
if ($this->isFinalized) {
|
|
return false;
|
|
}
|
|
|
|
if (strlen($this->streamFilePath) > 0) {
|
|
closeStream();
|
|
}
|
|
|
|
$this->streamFile = tempnam(sys_get_temp_dir(), 'ZipStream');
|
|
$this->streamData = fopen($this->streamFile, 'wb');
|
|
$this->streamFilePath = $filePath;
|
|
$this->streamTimestamp = $timestamp;
|
|
$this->streamFileComment = $fileComment;
|
|
$this->streamFileLength = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add data to the open stream.
|
|
*
|
|
* @param string $data
|
|
* @return $length bytes added or FALSE if the archive is finalized or there are no open stream.
|
|
*/
|
|
public function addStreamData($data)
|
|
{
|
|
if ($this->isFinalized || strlen($this->streamFilePath) == 0) {
|
|
return false;
|
|
}
|
|
|
|
$length = fwrite($this->streamData, $data, strlen($data));
|
|
if ($length != strlen($data)) {
|
|
die("<p>Length mismatch</p>\n");
|
|
}
|
|
$this->streamFileLength += $length;
|
|
|
|
return $length;
|
|
}
|
|
|
|
/**
|
|
* Close the current stream.
|
|
*
|
|
* @return bool $success
|
|
*/
|
|
public function closeStream()
|
|
{
|
|
if ($this->isFinalized || strlen($this->streamFilePath) == 0) {
|
|
return false;
|
|
}
|
|
|
|
fflush($this->streamData);
|
|
fclose($this->streamData);
|
|
|
|
$this->processFile($this->streamFile, $this->streamFilePath, $this->streamTimestamp, $this->streamFileComment);
|
|
|
|
$this->streamData = null;
|
|
$this->streamFilePath = null;
|
|
$this->streamTimestamp = null;
|
|
$this->streamFileComment = null;
|
|
$this->streamFileLength = 0;
|
|
|
|
// Windows is a little slow at times, so a millisecond later, we can unlink this.
|
|
unlink($this->streamFile);
|
|
|
|
$this->streamFile = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
private function processFile($dataFile, $filePath, $timestamp = 0, $fileComment = null)
|
|
{
|
|
if ($this->isFinalized) {
|
|
return false;
|
|
}
|
|
|
|
$tempzip = tempnam(sys_get_temp_dir(), 'ZipStream');
|
|
|
|
$zip = new ZipArchive;
|
|
if ($zip->open($tempzip) === true) {
|
|
$zip->addFile($dataFile, 'file');
|
|
$zip->close();
|
|
}
|
|
|
|
$file_handle = fopen($tempzip, 'rb');
|
|
$stats = fstat($file_handle);
|
|
$eof = $stats['size'] - 72;
|
|
|
|
fseek($file_handle, 6);
|
|
|
|
$gpFlags = fread($file_handle, 2);
|
|
$gzType = fread($file_handle, 2);
|
|
fread($file_handle, 4);
|
|
$fileCRC32 = fread($file_handle, 4);
|
|
$v = unpack('Vval', fread($file_handle, 4));
|
|
$gzLength = $v['val'];
|
|
$v = unpack('Vval', fread($file_handle, 4));
|
|
$dataLength = $v['val'];
|
|
|
|
$this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, self::EXT_FILE_ATTR_FILE);
|
|
|
|
fseek($file_handle, 34);
|
|
$pos = 34;
|
|
|
|
while (! feof($file_handle) && $pos < $eof) {
|
|
$datalen = $this->streamChunkSize;
|
|
if ($pos + $this->streamChunkSize > $eof) {
|
|
$datalen = $eof - $pos;
|
|
}
|
|
echo fread($file_handle, $datalen);
|
|
$pos += $datalen;
|
|
flush();
|
|
}
|
|
|
|
fclose($file_handle);
|
|
unlink($tempzip);
|
|
}
|
|
|
|
/**
|
|
* Close the archive.
|
|
* A closed archive can no longer have new files added to it.
|
|
* @return bool $success
|
|
*/
|
|
public function finalize()
|
|
{
|
|
if (! $this->isFinalized) {
|
|
if (strlen($this->streamFilePath) > 0) {
|
|
$this->closeStream();
|
|
}
|
|
|
|
$cdRecSize = pack('v', count($this->cdRec));
|
|
|
|
$cd = implode('', $this->cdRec);
|
|
echo $cd;
|
|
echo self::ZIP_END_OF_CENTRAL_DIRECTORY;
|
|
echo $cdRecSize.$cdRecSize;
|
|
echo pack('VV', strlen($cd), $this->offset);
|
|
if (! empty($this->zipComment)) {
|
|
echo pack('v', strlen($this->zipComment));
|
|
echo $this->zipComment;
|
|
} else {
|
|
echo "\x00\x00";
|
|
}
|
|
|
|
flush();
|
|
|
|
$this->isFinalized = true;
|
|
$cd = null;
|
|
$this->cdRec = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculate the 2 byte dostime used in the zip entries.
|
|
*
|
|
* @param int $timestamp
|
|
* @return 2-byte encoded DOS Date
|
|
*/
|
|
private function getDosTime($timestamp = 0)
|
|
{
|
|
$timestamp = (int) $timestamp;
|
|
$oldTZ = @date_default_timezone_get();
|
|
date_default_timezone_set('UTC');
|
|
$date = ($timestamp == 0 ? getdate() : getdate($timestamp));
|
|
date_default_timezone_set($oldTZ);
|
|
if ($date['year'] >= 1980) {
|
|
return pack('V', (($date['mday'] + ($date['mon'] << 5) + (($date['year'] - 1980) << 9)) << 16) |
|
|
(($date['seconds'] >> 1) + ($date['minutes'] << 5) + ($date['hours'] << 11)));
|
|
}
|
|
|
|
return "\x00\x00\x00\x00";
|
|
}
|
|
|
|
/**
|
|
* Build the Zip file structures.
|
|
*
|
|
* @param string $filePath
|
|
* @param string $fileComment
|
|
* @param string $gpFlags
|
|
* @param string $gzType
|
|
* @param int $timestamp
|
|
* @param string $fileCRC32
|
|
* @param int $gzLength
|
|
* @param int $dataLength
|
|
* @param int $extFileAttr Use self::EXT_FILE_ATTR_FILE for files, self::EXT_FILE_ATTR_DIR for Directories.
|
|
*/
|
|
private function buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr)
|
|
{
|
|
$filePath = str_replace('\\', '/', $filePath);
|
|
$fileCommentLength = (empty($fileComment) ? 0 : strlen($fileComment));
|
|
$timestamp = (int) $timestamp;
|
|
$timestamp = ($timestamp == 0 ? time() : $timestamp);
|
|
|
|
$dosTime = $this->getDosTime($timestamp);
|
|
$tsPack = pack('V', $timestamp);
|
|
|
|
$ux = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00";
|
|
|
|
if (! isset($gpFlags) || strlen($gpFlags) != 2) {
|
|
$gpFlags = "\x00\x00";
|
|
}
|
|
|
|
$isFileUTF8 = mb_check_encoding($filePath, 'UTF-8') && ! mb_check_encoding($filePath, 'ASCII');
|
|
$isCommentUTF8 = ! empty($fileComment) && mb_check_encoding($fileComment, 'UTF-8') && ! mb_check_encoding($fileComment, 'ASCII');
|
|
if ($isFileUTF8 || $isCommentUTF8) {
|
|
$flag = 0;
|
|
$gpFlagsV = unpack('vflags', $gpFlags);
|
|
if (isset($gpFlagsV['flags'])) {
|
|
$flag = $gpFlagsV['flags'];
|
|
}
|
|
$gpFlags = pack('v', $flag | (1 << 11));
|
|
}
|
|
|
|
$header = $gpFlags.$gzType.$dosTime.$fileCRC32
|
|
.pack('VVv', $gzLength, $dataLength, strlen($filePath)); // File name length
|
|
|
|
$zipEntry = self::ZIP_LOCAL_FILE_HEADER;
|
|
$zipEntry .= self::ATTR_VERSION_TO_EXTRACT;
|
|
$zipEntry .= $header;
|
|
$zipEntry .= $this->addExtraField ? "\x1C\x00" : "\x00\x00"; // Extra field length
|
|
$zipEntry .= $filePath; // FileName
|
|
// Extra fields
|
|
if ($this->addExtraField) {
|
|
$zipEntry .= "\x55\x54\x09\x00\x03".$tsPack.$tsPack.$ux;
|
|
}
|
|
|
|
echo $zipEntry;
|
|
|
|
$cdEntry = self::ZIP_CENTRAL_FILE_HEADER;
|
|
$cdEntry .= self::ATTR_MADE_BY_VERSION;
|
|
$cdEntry .= ($dataLength === 0 ? "\x0A\x00" : self::ATTR_VERSION_TO_EXTRACT);
|
|
$cdEntry .= $header;
|
|
$cdEntry .= $this->addExtraField ? "\x18\x00" : "\x00\x00"; // Extra field length
|
|
$cdEntry .= pack('v', $fileCommentLength); // File comment length
|
|
$cdEntry .= "\x00\x00"; // Disk number start
|
|
$cdEntry .= "\x00\x00"; // internal file attributes
|
|
$cdEntry .= $extFileAttr; // External file attributes
|
|
$cdEntry .= pack('V', $this->offset); // Relative offset of local header
|
|
$cdEntry .= $filePath; // FileName
|
|
// Extra fields
|
|
if ($this->addExtraField) {
|
|
$cdEntry .= "\x55\x54\x05\x00\x03".$tsPack.$ux;
|
|
}
|
|
|
|
if (! empty($fileComment)) {
|
|
$cdEntry .= $fileComment; // Comment
|
|
}
|
|
|
|
$this->cdRec[] = $cdEntry;
|
|
$this->offset += strlen($zipEntry) + $gzLength;
|
|
}
|
|
|
|
/**
|
|
* Join $file to $dir path, and clean up any excess slashes.
|
|
*
|
|
* @param string $dir
|
|
* @param string $file
|
|
*/
|
|
public static function pathJoin($dir, $file)
|
|
{
|
|
if (empty($dir) || empty($file)) {
|
|
return self::getRelativePath($dir.$file);
|
|
}
|
|
|
|
return self::getRelativePath($dir.'/'.$file);
|
|
}
|
|
|
|
/**
|
|
* Clean up a path, removing any unnecessary elements such as /./, // or redundant ../ segments.
|
|
* If the path starts with a "/", it is deemed an absolute path and any /../ in the beginning is stripped off.
|
|
* The returned path will not end in a "/".
|
|
*
|
|
* @param string $path The path to clean up
|
|
* @return string the clean path
|
|
*/
|
|
public static function getRelativePath($path)
|
|
{
|
|
$path = preg_replace("#/+\.?/+#", '/', str_replace('\\', '/', $path));
|
|
$dirs = explode('/', rtrim(preg_replace('#^(?:\./)+#', '', $path), '/'));
|
|
|
|
$offset = 0;
|
|
$sub = 0;
|
|
$subOffset = 0;
|
|
$root = '';
|
|
|
|
if (empty($dirs[0])) {
|
|
$root = '/';
|
|
$dirs = array_splice($dirs, 1);
|
|
} elseif (preg_match('#[A-Za-z]:#', $dirs[0])) {
|
|
$root = strtoupper($dirs[0]).'/';
|
|
$dirs = array_splice($dirs, 1);
|
|
}
|
|
|
|
$newDirs = [];
|
|
foreach ($dirs as $dir) {
|
|
if ($dir !== '..') {
|
|
$subOffset--;
|
|
$newDirs[++$offset] = $dir;
|
|
} else {
|
|
$subOffset++;
|
|
if (--$offset < 0) {
|
|
$offset = 0;
|
|
if ($subOffset > $sub) {
|
|
$sub++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($root)) {
|
|
$root = str_repeat('../', $sub);
|
|
}
|
|
|
|
return $root.implode('/', array_slice($newDirs, 0, $offset));
|
|
}
|
|
}
|