2015-08-30 15:01:12 +02:00
< ? 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
*/
2016-09-30 00:26:31 +02:00
class ZipStream
{
2015-10-25 03:35:37 +01:00
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 ;
2016-09-30 00:26:31 +02:00
private $cdRec = []; // central directory
2015-10-25 03:35:37 +01:00
private $offset = 0 ;
2016-09-30 00:26:31 +02:00
private $isFinalized = false ;
private $addExtraField = true ;
2015-10-25 03:35:37 +01:00
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 " .
*/
2016-09-30 00:26:31 +02:00
function __construct ( $archiveName = " " , $contentType = " application/zip " )
{
2015-10-25 03:35:37 +01:00
if ( ! function_exists ( 'sys_get_temp_dir' )) {
2016-09-30 00:26:31 +02:00
die ( " ERROR: ZipStream " . self :: VERSION . " requires PHP version 5.2.1 or above. " );
2015-10-25 03:35:37 +01:00
}
$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> " )) {
2016-09-30 00:26:31 +02:00
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> " )) {
2015-10-25 03:35:37 +01:00
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 ();
}
}
}
2016-09-30 00:26:31 +02:00
function __destruct ()
{
$this -> isFinalized = true ;
2015-10-25 03:35:37 +01:00
$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 .
*/
2016-09-30 00:26:31 +02:00
function setExtraField ( $setExtraField = true )
{
$this -> addExtraField = ( $setExtraField === true );
2015-10-25 03:35:37 +01:00
}
/**
* Set Zip archive comment .
*
* @ param string $newComment New comment . null to clear .
* @ return bool $success
*/
2016-09-30 00:26:31 +02:00
public function setComment ( $newComment = null )
{
2015-10-25 03:35:37 +01:00
if ( $this -> isFinalized ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
$this -> zipComment = $newComment ;
2016-09-30 00:26:31 +02:00
return true ;
2015-10-25 03:35:37 +01:00
}
/**
* 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
*/
2016-09-30 00:26:31 +02:00
public function addDirectory ( $directoryPath , $timestamp = 0 , $fileComment = null )
{
2015-10-25 03:35:37 +01:00
if ( $this -> isFinalized ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
$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 );
2016-09-30 00:26:31 +02:00
return true ;
2015-10-25 03:35:37 +01:00
}
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
/**
* 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
*/
2016-09-30 00:26:31 +02:00
public function addFile ( $data , $filePath , $timestamp = 0 , $fileComment = null )
{
2015-10-25 03:35:37 +01:00
if ( $this -> isFinalized ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
if ( is_resource ( $data ) && get_resource_type ( $data ) == " stream " ) {
$this -> addLargeFile ( $data , $filePath , $timestamp , $fileComment );
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
$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 );
print ( $gzData );
2016-09-30 00:26:31 +02:00
return true ;
2015-10-25 03:35:37 +01:00
}
/**
* 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 .
*/
2016-09-30 00:26:31 +02:00
public function addDirectoryContent ( $realPath , $zipPath , $recursive = true , $followSymlinks = true , & $addedFiles = [])
{
2015-10-25 03:35:37 +01:00
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 ());
2016-09-30 00:26:31 +02:00
if ( file_exists ( $newRealPath ) && ( $followSymlinks === true || ! is_link ( $newRealPath ))) {
2015-10-25 03:35:37 +01:00
if ( $file -> isFile ()) {
$addedFiles [ realpath ( $newRealPath )] = $newZipPath ;
$this -> addLargeFile ( $newRealPath , $newZipPath );
2016-09-30 00:26:31 +02:00
} else if ( $recursive === true ) {
2015-10-25 03:35:37 +01:00
$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
*/
2016-09-30 00:26:31 +02:00
public function addLargeFile ( $dataFile , $filePath , $timestamp = 0 , $fileComment = null )
{
2015-10-25 03:35:37 +01:00
if ( $this -> isFinalized ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
if ( is_string ( $dataFile ) && is_file ( $dataFile )) {
$this -> processFile ( $dataFile , $filePath , $timestamp , $fileComment );
} else if ( 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 );
}
2016-09-30 00:26:31 +02:00
return true ;
2015-10-25 03:35:37 +01:00
}
/**
* 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
*/
2016-09-30 00:26:31 +02:00
public function openStream ( $filePath , $timestamp = 0 , $fileComment = null )
{
2015-10-25 03:35:37 +01:00
if ( ! function_exists ( 'sys_get_temp_dir' )) {
2016-09-30 00:26:31 +02:00
die ( " ERROR: Zip " . self :: VERSION . " requires PHP version 5.2.1 or above if large files are used. " );
2015-10-25 03:35:37 +01:00
}
if ( $this -> isFinalized ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
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 ;
2016-09-30 00:26:31 +02:00
return true ;
2015-10-25 03:35:37 +01:00
}
/**
* 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 .
*/
2016-09-30 00:26:31 +02:00
public function addStreamData ( $data )
{
2015-10-25 03:35:37 +01:00
if ( $this -> isFinalized || strlen ( $this -> streamFilePath ) == 0 ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
$length = fwrite ( $this -> streamData , $data , strlen ( $data ));
if ( $length != strlen ( $data )) {
2016-09-30 00:26:31 +02:00
die ( " <p>Length mismatch</p> \n " );
2015-10-25 03:35:37 +01:00
}
$this -> streamFileLength += $length ;
return $length ;
}
/**
* Close the current stream .
*
* @ return bool $success
*/
2016-09-30 00:26:31 +02:00
public function closeStream ()
{
2015-10-25 03:35:37 +01:00
if ( $this -> isFinalized || strlen ( $this -> streamFilePath ) == 0 ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
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 ;
2016-09-30 00:26:31 +02:00
return true ;
2015-10-25 03:35:37 +01:00
}
2016-09-30 00:26:31 +02:00
private function processFile ( $dataFile , $filePath , $timestamp = 0 , $fileComment = null )
{
2015-10-25 03:35:37 +01:00
if ( $this -> isFinalized ) {
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
$tempzip = tempnam ( sys_get_temp_dir (), 'ZipStream' );
$zip = new ZipArchive ;
2016-09-30 00:26:31 +02:00
if ( $zip -> open ( $tempzip ) === true ) {
2015-10-25 03:35:37 +01:00
$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
*/
2016-09-30 00:26:31 +02:00
public function finalize ()
{
2015-10-25 03:35:37 +01:00
if ( ! $this -> isFinalized ) {
if ( strlen ( $this -> streamFilePath ) > 0 ) {
$this -> closeStream ();
}
$cdRecSize = pack ( " v " , sizeof ( $this -> cdRec ));
$cd = implode ( " " , $this -> cdRec );
print ( $cd );
print ( self :: ZIP_END_OF_CENTRAL_DIRECTORY );
print ( $cdRecSize . $cdRecSize );
print ( pack ( " VV " , strlen ( $cd ), $this -> offset ));
if ( ! empty ( $this -> zipComment )) {
print ( pack ( " v " , strlen ( $this -> zipComment )));
print ( $this -> zipComment );
} else {
print ( " \x00 \x00 " );
}
flush ();
2016-09-30 00:26:31 +02:00
$this -> isFinalized = true ;
2015-10-25 03:35:37 +01:00
$cd = null ;
$this -> cdRec = null ;
2016-09-30 00:26:31 +02:00
return true ;
2015-10-25 03:35:37 +01:00
}
2016-09-30 00:26:31 +02:00
return false ;
2015-10-25 03:35:37 +01:00
}
/**
* Calculate the 2 byte dostime used in the zip entries .
*
* @ param int $timestamp
* @ return 2 - byte encoded DOS Date
*/
2016-09-30 00:26:31 +02:00
private function getDosTime ( $timestamp = 0 )
{
2015-10-25 03:35:37 +01:00
$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 integer $extFileAttr Use self :: EXT_FILE_ATTR_FILE for files , self :: EXT_FILE_ATTR_DIR for Directories .
*/
2016-09-30 00:26:31 +02:00
private function buildZipEntry ( $filePath , $fileComment , $gpFlags , $gzType , $timestamp , $fileCRC32 , $gzLength , $dataLength , $extFileAttr )
{
2015-10-25 03:35:37 +01:00
$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 ;
}
print ( $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
*/
2016-09-30 00:26:31 +02:00
public static function pathJoin ( $dir , $file )
{
2015-10-25 03:35:37 +01:00
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
*/
2016-09-30 00:26:31 +02:00
public static function getRelativePath ( $path )
{
2015-10-25 03:35:37 +01:00
$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 );
} else if ( preg_match ( " #[A-Za-z]:# " , $dirs [ 0 ])) {
$root = strtoupper ( $dirs [ 0 ]) . " / " ;
$dirs = array_splice ( $dirs , 1 );
}
2016-09-30 00:26:31 +02:00
$newDirs = [];
2015-10-25 03:35:37 +01:00
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 ));
}
2015-08-30 15:01:12 +02:00
}