From 60df6f1c18820d34c162cbedbdb19af80cd6bbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Wed, 2 Jan 2013 21:50:44 +0100 Subject: [PATCH] Allow metadata updating for epub. Disabled by default for now. fix #18 --- CHANGELOG | 1 + README | 5 +- book.php | 31 ++ config_default.php | 7 + data.php | 2 +- fetch.php | 6 + php-epub-meta/LICENSE | 19 + php-epub-meta/epub.php | 608 +++++++++++++++++++++++++++ php-epub-meta/tbszip.php | 883 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 1560 insertions(+), 2 deletions(-) create mode 100644 php-epub-meta/LICENSE create mode 100644 php-epub-meta/epub.php create mode 100644 php-epub-meta/tbszip.php diff --git a/CHANGELOG b/CHANGELOG index e2ad1bb..069f183 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ * Better handling of content type for book. Reported by Morg. * Upped the size of thumbnails for OPDS. They look way better with Mantano. * Add language in OPDS feed (show in Mantano for example). + * Update metadata on downloaded epub. Disabled by default (check config item cops_update_epub-metadata). 0.2.3 - 20121205 * Add a .htaccess to make it easier to use with Apache diff --git a/README b/README index 3e34c92..eba8759 100644 --- a/README +++ b/README @@ -147,11 +147,14 @@ It also seems to work with Stanza. * Locale message handling is inspired of http://www.mind-it.info/2010/02/22/a-simple-approach-to-localization-in-php/ * str_format function come from http://tmont.com/blargh/2010/1/string-format-in-php * All icons come from the package Web0 by naf1971 : http://naf1971.deviantart.com/art/Web0-182067054 - * All testers + * Thanks to all testers External libraries used : * JQuery : http://jquery.com/ * Fancyapps : http://fancyapps.com/fancybox/ + * Php-epub-meta : https://github.com/splitbrain/php-epub-meta with some modification by me + https://github.com/seblucas/php-epub-meta + * TbsZip : http://www.tinybutstrong.com/apps/tbszip/tbszip_help.html = Copyright & License = diff --git a/book.php b/book.php index 516c4d3..2e6112a 100644 --- a/book.php +++ b/book.php @@ -11,6 +11,7 @@ require_once('serie.php'); require_once('author.php'); require_once('tag.php'); require_once('data.php'); +require_once('php-epub-meta/epub.php'); // Silly thing because PHP forbid string concatenation in class const define ('SQL_BOOKS_LEFT_JOIN', "left outer join comments on comments.book = books.id @@ -259,6 +260,36 @@ class Book extends Base { } } + public function getUpdatedEpub ($idData) + { + $data = $this->getDataById ($idData); + + try + { + $epub = new EPub ($data->getLocalPath ()); + + $epub->Title ($this->title); + $authorArray = array (); + foreach ($this->getAuthors() as $author) { + $authorArray [$author->sort] = $author->name; + } + $epub->Authors ($authorArray); + $epub->Language ($this->getLanguages ()); + $epub->Description ($this->getComment (false)); + $epub->Subjects ($this->getTagsName ()); + $se = $this->getSerie (); + if (!is_null ($se)) { + $epub->Serie ($se->name); + $epub->SerieIndex ($this->seriesIndex); + } + $epub->download ($data->getFilename ()); + } + catch (Exception $e) + { + echo "Exception : " . $e->getMessage(); + } + } + public function getLinkArray () { global $config; diff --git a/config_default.php b/config_default.php index a304f0b..d9a56aa 100644 --- a/config_default.php +++ b/config_default.php @@ -127,4 +127,11 @@ * 0 : No */ $config['cops_use_fancyapps'] = "1"; + + /* + * Update Epub metadata before download + * 1 : Yes (enable) + * 0 : No + */ + $config['cops_update_epub-metadata'] = "1"; ?> \ No newline at end of file diff --git a/data.php b/data.php index b66c8b6..edf052e 100644 --- a/data.php +++ b/data.php @@ -100,7 +100,7 @@ class Data extends Base { $textData = "&data=" . $idData; } - if (preg_match ('/^\//', $config['calibre_directory'])) + if (preg_match ('/^\//', $config['calibre_directory']) || ($type == "epub" && $config['cops_update_epub-metadata'])) { if ($type != "jpg") $textData .= "&type=" . $type; return new Link ("fetch.php?id=$book->id" . $textData, $mime, $rel, $title); diff --git a/fetch.php b/fetch.php index a1f7fb8..f6340ed 100644 --- a/fetch.php +++ b/fetch.php @@ -87,6 +87,12 @@ break; } $file = $book->getFilePath ($type, $idData, true); + if ($type == "epub" && $config['cops_update_epub-metadata']) + { + $book->getUpdatedEpub ($idData); + return; + } + header('Content-Disposition: attachment; filename="' . basename ($file) . '"'); header ($config['cops_x_accel_redirect'] . ": " . $config['calibre_internal_directory'] . $file); ?> \ No newline at end of file diff --git a/php-epub-meta/LICENSE b/php-epub-meta/LICENSE new file mode 100644 index 0000000..128bf1f --- /dev/null +++ b/php-epub-meta/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 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/php-epub-meta/epub.php b/php-epub-meta/epub.php new file mode 100644 index 0000000..2814ce6 --- /dev/null +++ b/php-epub-meta/epub.php @@ -0,0 +1,608 @@ + + */ + +require_once('tbszip.php'); + +define ("METADATA_FILE", "META-INF/container.xml"); + +class EPub { + public $xml; //FIXME change to protected, later + protected $xpath; + protected $file; + protected $meta; + protected $zip; + protected $namespaces; + protected $imagetoadd=''; + + /** + * Constructor + * + * @param string $file path to epub file to work on + * @throws Exception if metadata could not be loaded + */ + public function __construct($file){ + // open file + $this->file = $file; + $this->zip = new clsTbsZip(); + if(!$this->zip->Open($this->file)){ + throw new Exception('Failed to read epub file'); + } + + // read container data + if (!$this->zip->FileExists(METADATA_FILE)) { + throw new Exception ("Unable to find metadata.xml"); + } + + $data = $this->zip->FileRead(METADATA_FILE); + if($data == false){ + throw new Exception('Failed to access epub container data'); + } + $xml = new DOMDocument(); + $xml->registerNodeClass('DOMElement','EPubDOMElement'); + $xml->loadXML($data); + $xpath = new EPubDOMXPath($xml); + $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]'); + $this->meta = $nodes->item(0)->attr('full-path'); + + // load metadata + if (!$this->zip->FileExists($this->meta)) { + throw new Exception ("Unable to find " . $this->meta); + } + + $data = $this->zip->FileRead($this->meta); + if(!$data){ + throw new Exception('Failed to access epub metadata'); + } + $this->xml = new DOMDocument(); + $this->xml->registerNodeClass('DOMElement','EPubDOMElement'); + $this->xml->loadXML($data); + $this->xml->formatOutput = true; + $this->xpath = new EPubDOMXPath($this->xml); + } + + /** + * file name getter + */ + public function file(){ + return $this->file; + } + + /** + * Close the epub file + */ + public function close (){ + $this->zip->FileCancelModif($this->meta); + // TODO : Add cancelation of cover image + $this->zip->Close (); + } + + /** + * Writes back all meta data changes + * TODO update + */ + public function save(){ + $zip = new ZipArchive(); + $res = @$zip->open($this->file, ZipArchive::CREATE); + if($res === false){ + throw new Exception('Failed to write back metadata'); + } + $zip->addFromString($this->meta,$this->xml->saveXML()); + // add the cover image + if($this->imagetoadd){ + $path = dirname('/'.$this->meta).'/php-epub-meta-cover.img'; // image path is relative to meta file + $path = ltrim($path,'/'); + + $zip->addFromString($path,file_get_contents($this->imagetoadd)); + $this->imagetoadd=''; + } + $zip->close(); + } + + /** + * Get the updated epub + */ + public function download($file){ + $this->zip->FileReplace($this->meta,$this->xml->saveXML()); + // add the cover image + if($this->imagetoadd){ + $path = dirname('/'.$this->meta).'/php-epub-meta-cover.img'; // image path is relative to meta file + $path = ltrim($path,'/'); + + $this->zip->FileReplace($path,file_get_contents($this->imagetoadd)); + $this->imagetoadd=''; + } + $this->zip->Flush(TBSZIP_DOWNLOAD, $file); + } + + + + /** + * Get or set the book author(s) + * + * Authors should be given with a "file-as" and a real name. The file as + * is used for sorting in e-readers. + * + * Example: + * + * array( + * 'Pratchett, Terry' => 'Terry Pratchett', + * 'Simpson, Jacqeline' => 'Jacqueline Simpson', + * ) + * + * @params array $authors + */ + public function Authors($authors=false){ + // set new data + if($authors !== false){ + // Author where given as a comma separated list + if(is_string($authors)){ + if($authors == ''){ + $authors = array(); + }else{ + $authors = explode(',',$authors); + $authors = array_map('trim',$authors); + } + } + + // delete existing nodes + $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); + foreach($nodes as $node) $node->delete(); + + // add new nodes + $parent = $this->xpath->query('//opf:metadata')->item(0); + foreach($authors as $as => $name){ + if(is_int($as)) $as = $name; //numeric array given + $node = $parent->newChild('dc:creator',$name); + $node->attr('opf:role', 'aut'); + $node->attr('opf:file-as', $as); + } + + $this->reparse(); + } + + // read current data + $rolefix = false; + $authors = array(); + $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); + if($nodes->length == 0){ + // no nodes where found, let's try again without role + $nodes = $this->xpath->query('//opf:metadata/dc:creator'); + $rolefix = true; + } + foreach($nodes as $node){ + $name = $node->nodeValue; + $as = $node->attr('opf:file-as'); + if(!$as){ + $as = $name; + $node->attr('opf:file-as',$as); + } + if($rolefix){ + $node->attr('opf:role','aut'); + } + $authors[$as] = $name; + } + return $authors; + } + + /** + * Set or get the book title + * + * @param string $title + */ + public function Title($title=false){ + return $this->getset('dc:title',$title); + } + + /** + * Set or get the book's language + * + * @param string $lang + */ + public function Language($lang=false){ + return $this->getset('dc:language',$lang); + } + + /** + * Set or get the book' publisher info + * + * @param string $publisher + */ + public function Publisher($publisher=false){ + return $this->getset('dc:publisher',$publisher); + } + + /** + * Set or get the book's copyright info + * + * @param string $rights + */ + public function Copyright($rights=false){ + return $this->getset('dc:rights',$rights); + } + + /** + * Set or get the book's description + * + * @param string $description + */ + public function Description($description=false){ + return $this->getset('dc:description',$description); + } + + /** + * Set or get the book's ISBN number + * + * @param string $isbn + */ + public function ISBN($isbn=false){ + return $this->getset('dc:identifier',$isbn,'opf:scheme','ISBN'); + } + + /** + * Set or get the Google Books ID + * + * @param string $google + */ + public function Google($google=false){ + return $this->getset('dc:identifier',$google,'opf:scheme','GOOGLE'); + } + + /** + * Set or get the Amazon ID of the book + * + * @param string $amazon + */ + public function Amazon($amazon=false){ + return $this->getset('dc:identifier',$amazon,'opf:scheme','AMAZON'); + } + + /** + * Set or get the Serie of the book + * + * @param string $serie + */ + public function Serie($serie=false){ + return $this->getset('opf:meta',$serie,'name','calibre:series','content'); + } + + /** + * Set or get the Serie Index of the book + * + * @param string $serieIndex + */ + public function SerieIndex($serieIndex=false){ + return $this->getset('opf:meta',$serieIndex,'name','calibre:series_index','content'); + } + + /** + * Set or get the book's subjects (aka. tags) + * + * Subject should be given as array, but a comma separated string will also + * be accepted. + * + * @param array $subjects + */ + public function Subjects($subjects=false){ + // setter + if($subjects !== false){ + if(is_string($subjects)){ + if($subjects === ''){ + $subjects = array(); + }else{ + $subjects = explode(',',$subjects); + $subjects = array_map('trim',$subjects); + } + } + + // delete previous + $nodes = $this->xpath->query('//opf:metadata/dc:subject'); + foreach($nodes as $node){ + $node->delete(); + } + // add new ones + $parent = $this->xpath->query('//opf:metadata')->item(0); + foreach($subjects as $subj){ + $node = $this->xml->createElement('dc:subject',htmlspecialchars($subj)); + $node = $parent->appendChild($node); + } + + $this->reparse(); + } + + //getter + $subjects = array(); + $nodes = $this->xpath->query('//opf:metadata/dc:subject'); + foreach($nodes as $node){ + $subjects[] = $node->nodeValue; + } + return $subjects; + } + + /** + * Read the cover data + * + * Returns an associative array with the following keys: + * + * mime - filetype (usually image/jpeg) + * data - the binary image data + * found - the internal path, or false if no image is set in epub + * + * When no image is set in the epub file, the binary data for a transparent + * GIF pixel is returned. + * + * When adding a new image this function return no or old data because the + * image contents are not in the epub file, yet. The image will be added when + * the save() method is called. + * + * @param string $path local filesystem path to a new cover image + * @param string $mime mime type of the given file + * @return array + */ + public function Cover($path=false, $mime=false){ + // set cover + if($path !== false){ + // remove current pointer + $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + foreach($nodes as $node) $node->delete(); + // remove previous manifest entries if they where made by us + $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="php-epub-meta-cover"]'); + foreach($nodes as $node) $node->delete(); + + if($path){ + // add pointer + $parent = $this->xpath->query('//opf:metadata')->item(0); + $node = $parent->newChild('opf:meta'); + $node->attr('opf:name','cover'); + $node->attr('opf:content','php-epub-meta-cover'); + + // add manifest + $parent = $this->xpath->query('//opf:manifest')->item(0); + $node = $parent->newChild('opf:item'); + $node->attr('id','php-epub-meta-cover'); + $node->attr('opf:href','php-epub-meta-cover.img'); + $node->attr('opf:media-type',$mime); + + // remember path for save action + $this->imagetoadd = $path; + } + + $this->reparse(); + } + + // load cover + $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + if(!$nodes->length) return $this->no_cover(); + $coverid = (String) $nodes->item(0)->attr('opf:content'); + if(!$coverid) return $this->no_cover(); + + $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]'); + if(!$nodes->length) return $this->no_cover(); + $mime = $nodes->item(0)->attr('opf:media-type'); + $path = $nodes->item(0)->attr('opf:href'); + $path = dirname('/'.$this->meta).'/'.$path; // image path is relative to meta file + $path = ltrim($path,'/'); + + $zip = new ZipArchive(); + if(!@$zip->open($this->file)){ + throw new Exception('Failed to read epub file'); + } + $data = $zip->getFromName($path); + + return array( + 'mime' => $mime, + 'data' => $data, + 'found' => $path + ); + } + + /** + * A simple getter/setter for simple meta attributes + * + * It should only be used for attributes that are expected to be unique + * + * @param string $item XML node to set/get + * @param string $value New node value + * @param string $att Attribute name + * @param string $aval Attribute value + * @param string $datt Destination attribute + */ + protected function getset($item,$value=false,$att=false,$aval=false,$datt=false){ + // construct xpath + $xpath = '//opf:metadata/'.$item; + if($att){ + $xpath .= "[@$att=\"$aval\"]"; + } + + // set value + if($value !== false){ + $value = htmlspecialchars($value); + $nodes = $this->xpath->query($xpath); + if($nodes->length == 1 ){ + if($value === ''){ + // the user want's to empty this value -> delete the node + $nodes->item(0)->delete(); + }else{ + // replace value + if ($datt){ + $nodes->item(0)->attr ($datt, $value); + }else{ + $nodes->item(0)->nodeValue = $value; + } + } + }else{ + // if there are multiple matching nodes for some reason delete + // them. we'll replace them all with our own single one + foreach($nodes as $n) $n->delete(); + // readd them + if($value){ + $parent = $this->xpath->query('//opf:metadata')->item(0); + + $node = $parent->newChild ($item); + if($att) $node->attr($att,$aval); + if ($datt){ + $node->attr ($datt, $value); + }else{ + $node->nodeValue = $value; + } + } + } + + $this->reparse(); + } + + // get value + $nodes = $this->xpath->query($xpath); + if($nodes->length){ + if ($datt){ + return $nodes->item(0)->attr ($datt); + }else{ + return $nodes->item(0)->nodeValue; + } + }else{ + return ''; + } + } + + /** + * Return a not found response for Cover() + */ + protected function no_cover(){ + return array( + 'data' => base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'), + 'mime' => 'image/gif', + 'found' => false + ); + } + + /** + * Reparse the DOM tree + * + * I had to rely on this because otherwise xpath failed to find the newly + * added nodes + */ + protected function reparse() { + $this->xml->loadXML($this->xml->saveXML()); + $this->xpath = new EPubDOMXPath($this->xml); + } +} + +class EPubDOMXPath extends DOMXPath { + public function __construct(DOMDocument $doc){ + parent::__construct($doc); + + if(is_a($doc->documentElement, 'EPubDOMElement')){ + foreach($doc->documentElement->namespaces as $ns => $url){ + $this->registerNamespace($ns,$url); + } + } + } +} + +class EPubDOMElement extends DOMElement { + public $namespaces = array( + 'n' => 'urn:oasis:names:tc:opendocument:xmlns:container', + 'opf' => 'http://www.idpf.org/2007/opf', + 'dc' => 'http://purl.org/dc/elements/1.1/' + ); + + + public function __construct($name, $value='', $namespaceURI=''){ + list($ns,$name) = $this->splitns($name); + $value = htmlspecialchars($value); + if(!$namespaceURI && $ns){ + $namespaceURI = $this->namespaces[$ns]; + } + parent::__construct($name, $value, $namespaceURI); + } + + + /** + * Create and append a new child + * + * Works with our epub namespaces and omits default namespaces + */ + public function newChild($name, $value=''){ + list($ns,$local) = $this->splitns($name); + if($ns){ + $nsuri = $this->namespaces[$ns]; + if($this->isDefaultNamespace($nsuri)){ + $name = $local; + $nsuri = ''; + } + } + + // this doesn't call the construcor: $node = $this->ownerDocument->createElement($name,$value); + $node = new EPubDOMElement($name,$value,$nsuri); + return $this->appendChild($node); + } + + /** + * Split given name in namespace prefix and local part + * + * @param string $name + * @return array (namespace, name) + */ + public function splitns($name){ + $list = explode(':',$name,2); + if(count($list) < 2) array_unshift($list,''); + return $list; + } + + /** + * Simple EPub namespace aware attribute accessor + */ + public function attr($attr,$value=null){ + list($ns,$attr) = $this->splitns($attr); + + $nsuri = ''; + if($ns){ + $nsuri = $this->namespaces[$ns]; + if(!$this->namespaceURI){ + if($this->isDefaultNamespace($nsuri)){ + $nsuri = ''; + } + }elseif($this->namespaceURI == $nsuri){ + $nsuri = ''; + } + } + + if(!is_null($value)){ + if($value === false){ + // delete if false was given + if($nsuri){ + $this->removeAttributeNS($nsuri,$attr); + }else{ + $this->removeAttribute($attr); + } + }else{ + // modify if value was given + if($nsuri){ + $this->setAttributeNS($nsuri,$attr,$value); + }else{ + $this->setAttribute($attr,$value); + } + } + }else{ + // return value if none was given + if($nsuri){ + return $this->getAttributeNS($nsuri,$attr); + }else{ + return $this->getAttribute($attr); + } + } + } + + /** + * Remove this node from the DOM + */ + public function delete(){ + $this->parentNode->removeChild($this); + } + +} + + diff --git a/php-epub-meta/tbszip.php b/php-epub-meta/tbszip.php new file mode 100644 index 0000000..ba1214d --- /dev/null +++ b/php-epub-meta/tbszip.php @@ -0,0 +1,883 @@ +Meth8Ok = extension_loaded('zlib'); // check if Zlib extension is available. This is need for compress and uncompress with method 8. + $this->DisplayError = true; + $this->ArchFile = ''; + $this->Error = false; + } + + function CreateNew($ArchName='new.zip') { + // Create a new virtual empty archive, the name will be the default name when the archive is flushed. + if (!isset($this->Meth8Ok)) $this->__construct(); // for PHP 4 compatibility + $this->Close(); // note that $this->ArchHnd is set to false here + $this->Error = false; + $this->ArchFile = $ArchName; + $this->ArchIsNew = true; + $bin = 'PK'.chr(05).chr(06).str_repeat(chr(0), 18); + $this->CdEndPos = strlen($bin) - 4; + $this->CdInfo = array('disk_num_curr'=>0, 'disk_num_cd'=>0, 'file_nbr_curr'=>0, 'file_nbr_tot'=>0, 'l_cd'=>0, 'p_cd'=>0, 'l_comm'=>0, 'v_comm'=>'', 'bin'=>$bin); + $this->CdPos = $this->CdInfo['p_cd']; + } + + function Open($ArchFile) { + // Open the zip archive + if (!isset($this->Meth8Ok)) $this->__construct(); // for PHP 4 compatibility + $this->Close(); // close handle and init info + $this->Error = false; + $this->ArchFile = $ArchFile; + $this->ArchIsNew = false; + // open the file + $this->ArchHnd = fopen($ArchFile, 'rb'); + $ok = !($this->ArchHnd===false); + if ($ok) $ok = $this->CentralDirRead(); + return $ok; + } + + function Close() { + if (isset($this->ArchHnd) and ($this->ArchHnd!==false)) fclose($this->ArchHnd); + $this->ArchFile = ''; + $this->ArchHnd = false; + $this->CdInfo = array(); + $this->CdFileLst = array(); + $this->CdFileNbr = 0; + $this->CdFileByName = array(); + $this->VisFileLst = array(); + $this->ArchCancelModif(); + } + + function ArchCancelModif() { + $this->LastReadComp = false; // compression of the last read file (1=compressed, 0=stored not compressed, -1= stored compressed but read uncompressed) + $this->LastReadIdx = false; // index of the last file read + $this->ReplInfo = array(); + $this->ReplByPos = array(); + $this->AddInfo = array(); + } + + function FileAdd($Name, $Data, $DataType=TBSZIP_STRING, $Compress=true) { + + if ($Data===false) return $this->FileCancelModif($Name, false); // Cancel a previously added file + + // Save information for adding a new file into the archive + $Diff = 30 + 46 + 2*strlen($Name); // size of the header + cd info + $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $Name); + if ($Ref===false) return false; + $Ref['name'] = $Name; + $this->AddInfo[] = $Ref; + return $Ref['res']; + + } + + function CentralDirRead() { + $cd_info = 'PK'.chr(05).chr(06); // signature of the Central Directory + $cd_pos = -22; + $this->_MoveTo($cd_pos, SEEK_END); + $b = $this->_ReadData(4); + if ($b!==$cd_info) return $this->RaiseError('The footer of the Central Directory is not found.'); + + $this->CdEndPos = ftell($this->ArchHnd) - 4; + $this->CdInfo = $this->CentralDirRead_End($cd_info); + $this->CdFileLst = array(); + $this->CdFileNbr = $this->CdInfo['file_nbr_curr']; + $this->CdPos = $this->CdInfo['p_cd']; + + if ($this->CdFileNbr<=0) return $this->RaiseError('No file found in the Central Directory.'); + if ($this->CdPos<=0) return $this->RaiseError('No position found for the Central Directory listing.'); + + $this->_MoveTo($this->CdPos); + for ($i=0;$i<$this->CdFileNbr;$i++) { + $x = $this->CentralDirRead_File($i); + if ($x!==false) { + $this->CdFileLst[$i] = $x; + $this->CdFileByName[$x['v_name']] = $i; + } + } + return true; + } + + function CentralDirRead_End($cd_info) { + $b = $cd_info.$this->_ReadData(18); + $x = array(); + $x['disk_num_curr'] = $this->_GetDec($b,4,2); // number of this disk + $x['disk_num_cd'] = $this->_GetDec($b,6,2); // number of the disk with the start of the central directory + $x['file_nbr_curr'] = $this->_GetDec($b,8,2); // total number of entries in the central directory on this disk + $x['file_nbr_tot'] = $this->_GetDec($b,10,2); // total number of entries in the central directory + $x['l_cd'] = $this->_GetDec($b,12,4); // size of the central directory + $x['p_cd'] = $this->_GetDec($b,16,4); // offset of start of central directory with respect to the starting disk number + $x['l_comm'] = $this->_GetDec($b,20,2); // .ZIP file comment length + $x['v_comm'] = $this->_ReadData($x['l_comm']); // .ZIP file comment + $x['bin'] = $b.$x['v_comm']; + return $x; + } + + function CentralDirRead_File($idx) { + + $b = $this->_ReadData(46); + + $x = $this->_GetHex($b,0,4); + if ($x!=='h:02014b50') return $this->RaiseError('Signature of file information not found in the Central Directory in position '.(ftell($this->ArchHnd)-46).' for file #'.$idx.'.'); + + $x = array(); + $x['vers_used'] = $this->_GetDec($b,4,2); + $x['vers_necess'] = $this->_GetDec($b,6,2); + $x['purp'] = $this->_GetBin($b,8,2); + $x['meth'] = $this->_GetDec($b,10,2); + $x['time'] = $this->_GetDec($b,12,2); + $x['date'] = $this->_GetDec($b,14,2); + $x['crc32'] = $this->_GetDec($b,16,4); + $x['l_data_c'] = $this->_GetDec($b,20,4); + $x['l_data_u'] = $this->_GetDec($b,24,4); + $x['l_name'] = $this->_GetDec($b,28,2); + $x['l_fields'] = $this->_GetDec($b,30,2); + $x['l_comm'] = $this->_GetDec($b,32,2); + $x['disk_num'] = $this->_GetDec($b,34,2); + $x['int_file_att'] = $this->_GetDec($b,36,2); + $x['ext_file_att'] = $this->_GetDec($b,38,4); + $x['p_loc'] = $this->_GetDec($b,42,4); + $x['v_name'] = $this->_ReadData($x['l_name']); + $x['v_fields'] = $this->_ReadData($x['l_fields']); + $x['v_comm'] = $this->_ReadData($x['l_comm']); + + $x['bin'] = $b.$x['v_name'].$x['v_fields'].$x['v_comm']; + + return $x; + } + + function RaiseError($Msg) { + if ($this->DisplayError) echo ''.get_class($this).' ERROR : '.$Msg.'
'."\r\n"; + $this->Error = $Msg; + return false; + } + + function Debug($FileHeaders=false) { + + $this->DisplayError = true; + + echo "
\r\n"; + echo "------------------
\r\n"; + echo "Central Directory:
\r\n"; + echo "------------------
\r\n"; + print_r($this->CdInfo); + + echo "
\r\n"; + echo "-----------------------------------
\r\n"; + echo "File List in the Central Directory:
\r\n"; + echo "-----------------------------------
\r\n"; + print_r($this->CdFileLst); + + if ($FileHeaders) { + echo "
\r\n"; + echo "------------------------------
\r\n"; + echo "File List in the Data Section:
\r\n"; + echo "------------------------------
\r\n"; + $idx = 0; + $pos = 0; + $this->_MoveTo($pos); + while ($ok = $this->_ReadFile($idx,false)) { + $this->VisFileLst[$idx]['debug_pos'] = $pos; + $pos = ftell($this->ArchHnd); + $idx++; + } + print_r($this->VisFileLst); + } + + } + + function FileExists($NameOrIdx) { + return ($this->FileGetIdx($NameOrIdx)!==false); + } + + function FileGetIdx($NameOrIdx) { + // Check if a file name, or a file index exists in the Central Directory, and return its index + if (is_string($NameOrIdx)) { + if (isset($this->CdFileByName[$NameOrIdx])) { + return $this->CdFileByName[$NameOrIdx]; + } else { + return false; + } + } else { + if (isset($this->CdFileLst[$NameOrIdx])) { + return $NameOrIdx; + } else { + return false; + } + } + } + + function FileGetIdxAdd($Name) { + // Check if a file name exists in the list of file to add, and return its index + if (!is_string($Name)) return false; + $idx_lst = array_keys($this->AddInfo); + foreach ($idx_lst as $idx) { + if ($this->AddInfo[$idx]['name']===$Name) return $idx; + } + return false; + } + + function FileRead($NameOrIdx, $Uncompress=true) { + + $this->LastReadComp = false; // means the file is not found + $this->LastReadIdx = false; + + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.'); + + $pos = $this->CdFileLst[$idx]['p_loc']; + $this->_MoveTo($pos); + + $this->LastReadIdx = $idx; // Can be usefull to get the idx + + $Data = $this->_ReadFile($idx, true); + + // Manage uncompression + $Comp = 1; // means the contents stays compressed + $meth = $this->CdFileLst[$idx]['meth']; + if ($meth==8) { + if ($Uncompress) { + if ($this->Meth8Ok) { + $Data = gzinflate($Data); + $Comp = -1; // means uncompressed + } else { + $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because extension Zlib is not installed.'); + } + } + } elseif($meth==0) { + $Comp = 0; // means stored without compression + } else { + if ($Uncompress) $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because it is compressed with method '.$meth.'.'); + } + $this->LastReadComp = $Comp; + + return $Data; + + } + + function _ReadFile($idx, $ReadData) { + // read the file header (and maybe the data ) in the archive, assuming the cursor in at a new file position + + $b = $this->_ReadData(30); + + $x = $this->_GetHex($b,0,4); + if ($x!=='h:04034b50') return $this->RaiseError('Signature of file information not found in the Data Section in position '.(ftell($this->ArchHnd)-30).' for file #'.$idx.'.'); + + $x = array(); + $x['vers'] = $this->_GetDec($b,4,2); + $x['purp'] = $this->_GetBin($b,6,2); + $x['meth'] = $this->_GetDec($b,8,2); + $x['time'] = $this->_GetDec($b,10,2); + $x['date'] = $this->_GetDec($b,12,2); + $x['crc32'] = $this->_GetDec($b,14,4); + $x['l_data_c'] = $this->_GetDec($b,18,4); + $x['l_data_u'] = $this->_GetDec($b,22,4); + $x['l_name'] = $this->_GetDec($b,26,2); + $x['l_fields'] = $this->_GetDec($b,28,2); + $x['v_name'] = $this->_ReadData($x['l_name']); + $x['v_fields'] = $this->_ReadData($x['l_fields']); + + $x['bin'] = $b.$x['v_name'].$x['v_fields']; + + // Read Data + $len_cd = $this->CdFileLst[$idx]['l_data_c']; + if ($x['l_data_c']==0) { + // Sometimes, the size is not specified in the local information. + $len = $len_cd; + } else { + $len = $x['l_data_c']; + if ($len!=$len_cd) { + //echo "TbsZip Warning: Local information for file #".$idx." says len=".$len.", while Central Directory says len=".$len_cd."."; + } + } + + if ($ReadData) { + $Data = $this->_ReadData($len); + } else { + $this->_MoveTo($len, SEEK_CUR); + } + + // Description information + $desc_ok = ($x['purp'][2+3]=='1'); + if ($desc_ok) { + $b = $this->_ReadData(16); + $x['desc_bin'] = $b; + $x['desc_sign'] = $this->_GetHex($b,0,4); // not specified in the documentation sign=h:08074b50 + $x['desc_crc32'] = $this->_GetDec($b,4,4); + $x['desc_l_data_c'] = $this->_GetDec($b,8,4); + $x['desc_l_data_u'] = $this->_GetDec($b,12,4); + } + + // Save file info without the data + $this->VisFileLst[$idx] = $x; + + // Return the info + if ($ReadData) { + return $Data; + } else { + return true; + } + + } + + function FileReplace($NameOrIdx, $Data, $DataType=TBSZIP_STRING, $Compress=true) { + // Store replacement information. + + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.'); + + $pos = $this->CdFileLst[$idx]['p_loc']; + + if ($Data===false) { + // file to delete + $this->ReplInfo[$idx] = false; + $Result = true; + } else { + // file to replace + $Diff = - $this->CdFileLst[$idx]['l_data_c']; + $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx); + if ($Ref===false) return false; + $this->ReplInfo[$idx] = $Ref; + $Result = $Ref['res']; + } + + $this->ReplByPos[$pos] = $idx; + + return $Result; + + } + + function FileCancelModif($NameOrIdx, $ReplacedAndDeleted=true) { + // cancel added, modified or deleted modifications on a file in the archive + // return the number of cancels + + $nbr = 0; + + if ($ReplacedAndDeleted) { + // replaced or deleted files + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx!==false) { + if (isset($this->ReplInfo[$idx])) { + $pos = $this->CdFileLst[$idx]['p_loc']; + unset($this->ReplByPos[$pos]); + unset($this->ReplInfo[$idx]); + $nbr++; + } + } + } + + // added files + $idx = $this->FileGetIdxAdd($NameOrIdx); + if ($idx!==false) { + unset($this->AddInfo[$idx]); + $nbr++; + } + + return $nbr; + + } + + function Flush($Render=TBSZIP_DOWNLOAD, $File='', $ContentType='') { + + if ( ($File!=='') && ($this->ArchFile===$File)) { + $this->RaiseError('Method Flush() cannot overwrite the current opened archive: \''.$File.'\''); // this makes corrupted zip archives without PHP error. + return false; + } + + $ArchPos = 0; + $Delta = 0; + $FicNewPos = array(); + $DelLst = array(); // idx of deleted files + $DeltaCdLen = 0; // delta of the CD's size + + $now = time(); + $date = $this->_MsDos_Date($now); + $time = $this->_MsDos_Time($now); + + if (!$this->OutputOpen($Render, $File, $ContentType)) return false; + + // output modified zipped files and unmodified zipped files that are beetween them + ksort($this->ReplByPos); + foreach ($this->ReplByPos as $ReplPos => $ReplIdx) { + // output data from the zip archive which is before the data to replace + $this->OutputFromArch($ArchPos, $ReplPos); + // get current file information + if (!isset($this->VisFileLst[$ReplIdx])) $this->_ReadFile($ReplIdx, false); + $FileInfo =& $this->VisFileLst[$ReplIdx]; + $b1 = $FileInfo['bin']; + if (isset($FileInfo['desc_bin'])) { + $b2 = $FileInfo['desc_bin']; + } else { + $b2 = ''; + } + $info_old_len = strlen($b1) + $this->CdFileLst[$ReplIdx]['l_data_c'] + strlen($b2); // $FileInfo['l_data_c'] may have a 0 value in some archives + // get replacement information + $ReplInfo =& $this->ReplInfo[$ReplIdx]; + if ($ReplInfo===false) { + // The file is to be deleted + $Delta = $Delta - $info_old_len; // headers and footers are also deleted + $DelLst[$ReplIdx] = true; + } else { + // prepare the header of the current file + $this->_DataPrepare($ReplInfo); // get data from external file if necessary + $this->_PutDec($b1, $time, 10, 2); // time + $this->_PutDec($b1, $date, 12, 2); // date + $this->_PutDec($b1, $ReplInfo['crc32'], 14, 4); // crc32 + $this->_PutDec($b1, $ReplInfo['len_c'], 18, 4); // l_data_c + $this->_PutDec($b1, $ReplInfo['len_u'], 22, 4); // l_data_u + if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 8, 2); // meth + // prepare the bottom description if the zipped file, if any + if ($b2!=='') { + $this->_PutDec($b2, $ReplInfo['crc32'], 4, 4); // crc32 + $this->_PutDec($b2, $ReplInfo['len_c'], 8, 4); // l_data_c + $this->_PutDec($b2, $ReplInfo['len_u'], 12, 4); // l_data_u + } + // output data + $this->OutputFromString($b1.$ReplInfo['data'].$b2); + unset($ReplInfo['data']); // save PHP memory + $Delta = $Delta + $ReplInfo['diff'] + $ReplInfo['len_c']; + } + // Update the delta of positions for zipped files which are physically after the currently replaced one + for ($i=0;$i<$this->CdFileNbr;$i++) { + if ($this->CdFileLst[$i]['p_loc']>$ReplPos) { + $FicNewPos[$i] = $this->CdFileLst[$i]['p_loc'] + $Delta; + } + } + // Update the current pos in the archive + $ArchPos = $ReplPos + $info_old_len; + } + + // Ouput all the zipped files that remain before the Central Directory listing + if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdPos); // ArchHnd is false if CreateNew() has been called + $ArchPos = $this->CdPos; + + // Output file to add + $AddNbr = count($this->AddInfo); + $AddDataLen = 0; // total len of added data (inlcuding file headers) + if ($AddNbr>0) { + $AddPos = $ArchPos + $Delta; // position of the start + $AddLst = array_keys($this->AddInfo); + foreach ($AddLst as $idx) { + $n = $this->_DataOuputAddedFile($idx, $AddPos); + $AddPos += $n; + $AddDataLen += $n; + } + } + + // Modifiy file information in the Central Directory for replaced files + $b2 = ''; + $old_cd_len = 0; + for ($i=0;$i<$this->CdFileNbr;$i++) { + $b1 = $this->CdFileLst[$i]['bin']; + $old_cd_len += strlen($b1); + if (!isset($DelLst[$i])) { + if (isset($FicNewPos[$i])) $this->_PutDec($b1, $FicNewPos[$i], 42, 4); // p_loc + if (isset($this->ReplInfo[$i])) { + $ReplInfo =& $this->ReplInfo[$i]; + $this->_PutDec($b1, $time, 12, 2); // time + $this->_PutDec($b1, $date, 14, 2); // date + $this->_PutDec($b1, $ReplInfo['crc32'], 16, 4); // crc32 + $this->_PutDec($b1, $ReplInfo['len_c'], 20, 4); // l_data_c + $this->_PutDec($b1, $ReplInfo['len_u'], 24, 4); // l_data_u + if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 10, 2); // meth + } + $b2 .= $b1; + } + } + $this->OutputFromString($b2); + $ArchPos += $old_cd_len; + $DeltaCdLen = $DeltaCdLen + strlen($b2) - $old_cd_len; + + // Output until Central Directory footer + if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdEndPos); // ArchHnd is false if CreateNew() has been called + + // Output file information of the Central Directory for added files + if ($AddNbr>0) { + $b2 = ''; + foreach ($AddLst as $idx) { + $b2 .= $this->AddInfo[$idx]['bin']; + } + $this->OutputFromString($b2); + $DeltaCdLen += strlen($b2); + } + + // Output Central Directory footer + $b2 = $this->CdInfo['bin']; + $DelNbr = count($DelLst); + if ( ($AddNbr>0) or ($DelNbr>0) ) { + // total number of entries in the central directory on this disk + $n = $this->_GetDec($b2, 8, 2); + $this->_PutDec($b2, $n + $AddNbr - $DelNbr, 8, 2); + // total number of entries in the central directory + $n = $this->_GetDec($b2, 10, 2); + $this->_PutDec($b2, $n + $AddNbr - $DelNbr, 10, 2); + // size of the central directory + $n = $this->_GetDec($b2, 12, 4); + $this->_PutDec($b2, $n + $DeltaCdLen, 12, 4); + $Delta = $Delta + $AddDataLen; + } + $this->_PutDec($b2, $this->CdPos+$Delta , 16, 4); // p_cd (offset of start of central directory with respect to the starting disk number) + $this->OutputFromString($b2); + + $this->OutputClose(); + + return true; + + } + + // ---------------- + // output functions + // ---------------- + + function OutputOpen($Render, $File, $ContentType) { + + if (($Render & TBSZIP_FILE)==TBSZIP_FILE) { + $this->OutputMode = TBSZIP_FILE; + if (''.$File=='') $File = basename($this->ArchFile).'.zip'; + $this->OutputHandle = @fopen($File, 'w'); + if ($this->OutputHandle===false) { + $this->RaiseError('Method Flush() cannot overwrite the target file \''.$File.'\'. This may not be a valid file path or the file may be locked by another process or because of a denied permission.'); + return false; + } + } elseif (($Render & TBSZIP_STRING)==TBSZIP_STRING) { + $this->OutputMode = TBSZIP_STRING; + $this->OutputSrc = ''; + } elseif (($Render & TBSZIP_DOWNLOAD)==TBSZIP_DOWNLOAD) { + $this->OutputMode = TBSZIP_DOWNLOAD; + // Output the file + if (''.$File=='') $File = basename($this->ArchFile); + if (($Render & TBSZIP_NOHEADER)==TBSZIP_NOHEADER) { + } else { + header ('Pragma: no-cache'); + if ($ContentType!='') header ('Content-Type: '.$ContentType); + header('Content-Disposition: attachment; filename="'.$File.'"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: public'); + header('Content-Description: File Transfer'); + header('Content-Transfer-Encoding: binary'); + $Len = $this->_EstimateNewArchSize(); + if ($Len!==false) header('Content-Length: '.$Len); + } + } + + return true; + + } + + function OutputFromArch($pos, $pos_stop) { + $len = $pos_stop - $pos; + if ($len<0) return; + $this->_MoveTo($pos); + $block = 1024; + while ($len>0) { + $l = min($len, $block); + $x = $this->_ReadData($l); + $this->OutputFromString($x); + $len = $len - $l; + } + unset($x); + } + + function OutputFromString($data) { + if ($this->OutputMode===TBSZIP_DOWNLOAD) { + echo $data; // donwload + } elseif ($this->OutputMode===TBSZIP_STRING) { + $this->OutputSrc .= $data; // to string + } elseif (TBSZIP_FILE) { + fwrite($this->OutputHandle, $data); // to file + } + } + + function OutputClose() { + if ( ($this->OutputMode===TBSZIP_FILE) && ($this->OutputHandle!==false) ) { + fclose($this->OutputHandle); + $this->OutputHandle = false; + } + } + + // ---------------- + // Reading functions + // ---------------- + + function _MoveTo($pos, $relative = SEEK_SET) { + fseek($this->ArchHnd, $pos, $relative); + } + + function _ReadData($len) { + if ($len>0) { + $x = fread($this->ArchHnd, $len); + return $x; + } else { + return ''; + } + } + + // ---------------- + // Take info from binary data + // ---------------- + + function _GetDec($txt, $pos, $len) { + $x = substr($txt, $pos, $len); + $z = 0; + for ($i=0;$i<$len;$i++) { + $asc = ord($x[$i]); + if ($asc>0) $z = $z + $asc*pow(256,$i); + } + return $z; + } + + function _GetHex($txt, $pos, $len) { + $x = substr($txt, $pos, $len); + return 'h:'.bin2hex(strrev($x)); + } + + function _GetBin($txt, $pos, $len) { + $x = substr($txt, $pos, $len); + $z = ''; + for ($i=0;$i<$len;$i++) { + $asc = ord($x[$i]); + if (isset($x[$i])) { + for ($j=0;$j<8;$j++) { + $z .= ($asc & pow(2,$j)) ? '1' : '0'; + } + } else { + $z .= '00000000'; + } + } + return 'b:'.$z; + } + + // ---------------- + // Put info into binary data + // ---------------- + + function _PutDec(&$txt, $val, $pos, $len) { + $x = ''; + for ($i=0;$i<$len;$i++) { + if ($val==0) { + $z = 0; + } else { + $z = intval($val % 256); + if (($val<0) && ($z!=0)) { // ($z!=0) is very important, example: val=-420085702 + // special opration for negative value. If the number id too big, PHP stores it into a signed integer. For example: crc32('coucou') => -256185401 instead of 4038781895. NegVal = BigVal - (MaxVal+1) = BigVal - 256^4 + $val = ($val - $z)/256 -1; + $z = 256 + $z; + } else { + $val = ($val - $z)/256; + } + } + $x .= chr($z); + } + $txt = substr_replace($txt, $x, $pos, $len); + } + + function _MsDos_Date($Timestamp = false) { + // convert a date-time timstamp into the MS-Dos format + $d = ($Timestamp===false) ? getdate() : getdate($Timestamp); + return (($d['year']-1980)*512) + ($d['mon']*32) + $d['mday']; + } + function _MsDos_Time($Timestamp = false) { + // convert a date-time timstamp into the MS-Dos format + $d = ($Timestamp===false) ? getdate() : getdate($Timestamp); + return ($d['hours']*2048) + ($d['minutes']*32) + intval($d['seconds']/2); // seconds are rounded to an even number in order to save 1 bit + } + + function _MsDos_Debug($date, $time) { + // Display the formated date and time. Just for debug purpose. + // date end time are encoded on 16 bits (2 bytes) : date = yyyyyyymmmmddddd , time = hhhhhnnnnnssssss + $y = ($date & 65024)/512 + 1980; + $m = ($date & 480)/32; + $d = ($date & 31); + $h = ($time & 63488)/2048; + $i = ($time & 1984)/32; + $s = ($time & 31) * 2; // seconds have been rounded to an even number in order to save 1 bit + return $y.'-'.str_pad($m,2,'0',STR_PAD_LEFT).'-'.str_pad($d,2,'0',STR_PAD_LEFT).' '.str_pad($h,2,'0',STR_PAD_LEFT).':'.str_pad($i,2,'0',STR_PAD_LEFT).':'.str_pad($s,2,'0',STR_PAD_LEFT); + } + + function _DataOuputAddedFile($Idx, $PosLoc) { + + $Ref =& $this->AddInfo[$Idx]; + $this->_DataPrepare($Ref); // get data from external file if necessary + + // Other info + $now = time(); + $date = $this->_MsDos_Date($now); + $time = $this->_MsDos_Time($now); + $len_n = strlen($Ref['name']); + $purp = 2048 ; // purpose // +8 to indicates that there is an extended local header + + // Header for file in the data section + $b = 'PK'.chr(03).chr(04).str_repeat(' ',26); // signature + $this->_PutDec($b,20,4,2); //vers = 20 + $this->_PutDec($b,$purp,6,2); // purp + $this->_PutDec($b,$Ref['meth'],8,2); // meth + $this->_PutDec($b,$time,10,2); // time + $this->_PutDec($b,$date,12,2); // date + $this->_PutDec($b,$Ref['crc32'],14,4); // crc32 + $this->_PutDec($b,$Ref['len_c'],18,4); // l_data_c + $this->_PutDec($b,$Ref['len_u'],22,4); // l_data_u + $this->_PutDec($b,$len_n,26,2); // l_name + $this->_PutDec($b,0,28,2); // l_fields + $b .= $Ref['name']; // name + $b .= ''; // fields + + // Output the data + $this->OutputFromString($b.$Ref['data']); + $OutputLen = strlen($b) + $Ref['len_c']; // new position of the cursor + unset($Ref['data']); // save PHP memory + + // Information for file in the Central Directory + $b = 'PK'.chr(01).chr(02).str_repeat(' ',42); // signature + $this->_PutDec($b,20,4,2); // vers_used = 20 + $this->_PutDec($b,20,6,2); // vers_necess = 20 + $this->_PutDec($b,$purp,8,2); // purp + $this->_PutDec($b,$Ref['meth'],10,2); // meth + $this->_PutDec($b,$time,12,2); // time + $this->_PutDec($b,$date,14,2); // date + $this->_PutDec($b,$Ref['crc32'],16,4); // crc32 + $this->_PutDec($b,$Ref['len_c'],20,4); // l_data_c + $this->_PutDec($b,$Ref['len_u'],24,4); // l_data_u + $this->_PutDec($b,$len_n,28,2); // l_name + $this->_PutDec($b,0,30,2); // l_fields + $this->_PutDec($b,0,32,2); // l_comm + $this->_PutDec($b,0,34,2); // disk_num + $this->_PutDec($b,0,36,2); // int_file_att + $this->_PutDec($b,0,38,4); // ext_file_att + $this->_PutDec($b,$PosLoc,42,4); // p_loc + $b .= $Ref['name']; // v_name + $b .= ''; // v_fields + $b .= ''; // v_comm + + $Ref['bin'] = $b; + + return $OutputLen; + + } + + function _DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx) { + + if (is_array($Compress)) { + $result = 2; + $meth = $Compress['meth']; + $len_u = $Compress['len_u']; + $crc32 = $Compress['crc32']; + $Compress = false; + } elseif ($Compress and ($this->Meth8Ok)) { + $result = 1; + $meth = 8; + $len_u = false; // means unknown + $crc32 = false; + } else { + $result = ($Compress) ? -1 : 0; + $meth = 0; + $len_u = false; + $crc32 = false; + $Compress = false; + } + + if ($DataType==TBSZIP_STRING) { + $path = false; + if ($Compress) { + // we compress now in order to save PHP memory + $len_u = strlen($Data); + $crc32 = crc32($Data); + $Data = gzdeflate($Data); + $len_c = strlen($Data); + } else { + $len_c = strlen($Data); + if ($len_u===false) { + $len_u = $len_c; + $crc32 = crc32($Data); + } + } + } else { + $path = $Data; + $Data = false; + if (file_exists($path)) { + $fz = filesize($path); + if ($len_u===false) $len_u = $fz; + $len_c = ($Compress) ? false : $fz; + } else { + return $this->RaiseError("Cannot add the file '".$path."' because it is not found."); + } + } + + // at this step $Data and $crc32 can be false only in case of external file, and $len_c is false only in case of external file to compress + return array('data'=>$Data, 'path'=>$path, 'meth'=>$meth, 'len_u'=>$len_u, 'len_c'=>$len_c, 'crc32'=>$crc32, 'diff'=>$Diff, 'res'=>$result); + + } + + function _DataPrepare(&$Ref) { + // returns the real size of data + if ($Ref['path']!==false) { + $Ref['data'] = file_get_contents($Ref['path']); + if ($Ref['crc32']===false) $Ref['crc32'] = crc32($Ref['data']); + if ($Ref['len_c']===false) { + // means the data must be compressed + $Ref['data'] = gzdeflate($Ref['data']); + $Ref['len_c'] = strlen($Ref['data']); + } + } + } + + function _EstimateNewArchSize($Optim=true) { + // Return the size of the new archive, or false if it cannot be calculated (because of external file that must be compressed before to be insered) + + if ($this->ArchIsNew) { + $Len = strlen($this->CdInfo['bin']); + } else { + $Len = filesize($this->ArchFile); + } + + // files to replace or delete + foreach ($this->ReplByPos as $i) { + $Ref =& $this->ReplInfo[$i]; + if ($Ref===false) { + // file to delete + $Info =& $this->CdFileLst[$i]; + if (!isset($this->VisFileLst[$i])) { + if ($Optim) return false; // if $Optimization is set to true, then we d'ont rewind to read information + $this->_MoveTo($Info['p_loc']); + $this->_ReadFile($i, false); + } + $Vis =& $this->VisFileLst[$i]; + $Len += -strlen($Vis['bin']) -strlen($Info['bin']) - $Info['l_data_c']; + if (isset($Vis['desc_bin'])) $Len += -strlen($Vis['desc_bin']); + } elseif ($Ref['len_c']===false) { + return false; // information not yet known + } else { + // file to replace + $Len += $Ref['len_c'] + $Ref['diff']; + } + } + + // files to add + $i_lst = array_keys($this->AddInfo); + foreach ($i_lst as $i) { + $Ref =& $this->AddInfo[$i]; + if ($Ref['len_c']===false) { + return false; // information not yet known + } else { + $Len += $Ref['len_c'] + $Ref['diff']; + } + } + + return $Len; + + } + +} \ No newline at end of file