From df1e1b3c17259782b4d947e32aca0bb828c39f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Sun, 9 Dec 2012 07:34:08 +0100 Subject: [PATCH 01/63] Use tbszip to allow to download an updated version of the epub without having to create one in a temporary directory --- epub.php | 52 +++- tbszip.php | 883 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 929 insertions(+), 6 deletions(-) create mode 100644 tbszip.php diff --git a/epub.php b/epub.php index 6895a67..2c58358 100644 --- a/epub.php +++ b/epub.php @@ -4,11 +4,17 @@ * * @author Andreas Gohr */ + +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=''; @@ -21,13 +27,17 @@ class EPub { public function __construct($file){ // open file $this->file = $file; - $zip = new ZipArchive(); - if(!@$zip->open($this->file)){ + $this->zip = new clsTbsZip(); + if(!$this->zip->Open($this->file)){ throw new Exception('Failed to read epub file'); } // read container data - $data = $zip->getFromName('META-INF/container.xml'); + 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'); } @@ -39,7 +49,11 @@ public function __construct($file){ $this->meta = $nodes->item(0)->attr('full-path'); // load metadata - $data = $zip->getFromName($this->meta); + 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'); } @@ -48,8 +62,6 @@ public function __construct($file){ $this->xml->loadXML($data); $this->xml->formatOutput = true; $this->xpath = new EPubDOMXPath($this->xml); - - $zip->close(); } /** @@ -58,9 +70,19 @@ public function __construct($file){ 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(); @@ -79,6 +101,24 @@ public function save(){ } $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) diff --git a/tbszip.php b/tbszip.php new file mode 100644 index 0000000..ba1214d --- /dev/null +++ b/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 From af8069fef5c3d2f6f624203df6ebdb1aa761e8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Mon, 24 Dec 2012 15:36:24 +0100 Subject: [PATCH 02/63] Add support of Calibre addition : Serie & Serie Index --- epub.php | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/epub.php b/epub.php index 2c58358..2d25cf7 100644 --- a/epub.php +++ b/epub.php @@ -75,7 +75,7 @@ public function file(){ * Close the epub file */ public function close (){ - $this->zip->FileCancelModif($this->meta) + $this->zip->FileCancelModif($this->meta); // TODO : Add cancelation of cover image $this->zip->Close (); } @@ -260,6 +260,24 @@ 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,'opf:name','calibre:series','opf:content'); + } + + /** + * Set or get the Serie Index of the book + * + * @param string $serieIndex + */ + public function SerieIndex($serieIndex=false){ + return $this->getset('meta',$serieIndex,'opf:name','calibre:series_index','opf:content'); + } + /** * Set or get the book's subjects (aka. tags) * @@ -390,8 +408,9 @@ public function Cover($path=false, $mime=false){ * @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){ + protected function getset($item,$value=false,$att=false,$aval=false,$datt=false){ // construct xpath $xpath = '//opf:metadata/'.$item; if($att){ @@ -408,7 +427,11 @@ protected function getset($item,$value=false,$att=false,$aval=false){ $nodes->item(0)->delete(); }else{ // replace value - $nodes->item(0)->nodeValue = $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 @@ -417,9 +440,14 @@ protected function getset($item,$value=false,$att=false,$aval=false){ // readd them if($value){ $parent = $this->xpath->query('//opf:metadata')->item(0); - $node = $this->xml->createElement($item,$value); + $node = $this->xml->createElement($item); $node = $parent->appendChild($node); if($att) $node->attr($att,$aval); + if ($datt){ + $node->attr ($datt, $value); + }else{ + $node->nodeValue = $value; + } } } @@ -429,7 +457,11 @@ protected function getset($item,$value=false,$att=false,$aval=false){ // get value $nodes = $this->xpath->query($xpath); if($nodes->length){ - return $nodes->item(0)->nodeValue; + if ($datt){ + return $nodes->item(0)->attr ($datt); + }else{ + return $nodes->item(0)->nodeValue; + } }else{ return ''; } From 5b5ad225b78a21a10666e18251462e0417976809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Wed, 26 Dec 2012 16:27:22 +0100 Subject: [PATCH 03/63] Fix xpath query & new meta adding --- epub.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/epub.php b/epub.php index 2d25cf7..2814ce6 100644 --- a/epub.php +++ b/epub.php @@ -266,7 +266,7 @@ public function Amazon($amazon=false){ * @param string $serie */ public function Serie($serie=false){ - return $this->getset('opf:meta',$serie,'opf:name','calibre:series','opf:content'); + return $this->getset('opf:meta',$serie,'name','calibre:series','content'); } /** @@ -275,7 +275,7 @@ public function Serie($serie=false){ * @param string $serieIndex */ public function SerieIndex($serieIndex=false){ - return $this->getset('meta',$serieIndex,'opf:name','calibre:series_index','opf:content'); + return $this->getset('opf:meta',$serieIndex,'name','calibre:series_index','content'); } /** @@ -440,8 +440,8 @@ protected function getset($item,$value=false,$att=false,$aval=false,$datt=false) // readd them if($value){ $parent = $this->xpath->query('//opf:metadata')->item(0); - $node = $this->xml->createElement($item); - $node = $parent->appendChild($node); + + $node = $parent->newChild ($item); if($att) $node->attr($att,$aval); if ($datt){ $node->attr ($datt, $value); From 6a23fecd93842bcbca7bcedc8318a690ab9007e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Thu, 31 Jan 2013 21:18:12 +0100 Subject: [PATCH 04/63] Add a way to set the Calibre identifier. --- epub.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/epub.php b/epub.php index 2814ce6..5d03a59 100644 --- a/epub.php +++ b/epub.php @@ -259,6 +259,15 @@ public function Google($google=false){ public function Amazon($amazon=false){ return $this->getset('dc:identifier',$amazon,'opf:scheme','AMAZON'); } + + /** + * Set or get the Calibre UUID of the book + * + * @param string $uuid + */ + public function Calibre($uuid=false){ + return $this->getset('dc:identifier',$uuid,'opf:scheme','calibre'); + } /** * Set or get the Serie of the book From f528f1d1a0ef3145a3111f826c4a3514faff83dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Thu, 31 Jan 2013 21:20:13 +0100 Subject: [PATCH 05/63] A little try to handle cover better (at least for my taste). --- epub.php | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/epub.php b/epub.php index 5d03a59..8ff585a 100644 --- a/epub.php +++ b/epub.php @@ -15,6 +15,7 @@ class EPub { protected $file; protected $meta; protected $zip; + protected $coverpath=''; protected $namespaces; protected $imagetoadd=''; @@ -407,6 +408,61 @@ public function Cover($path=false, $mime=false){ 'found' => $path ); } + + + public function Cover2($path=false, $mime=false){ + $hascover = true; + $item; + // load cover + $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + if(!$nodes->length){ + $hascover = false; + } else{ + $coverid = (String) $nodes->item(0)->attr('opf:content'); + if(!$coverid){ + $hascover = false; + } else{ + $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]'); + if(!$nodes->length){ + $hascover = false; + } else{ + $item = $nodes->item(0); + $mime = $item->attr('opf:media-type'); + $this->coverpath = $item->attr('opf:href'); + $this->coverpath = dirname('/'.$this->meta).'/'.$this->coverpath; // image path is relative to meta file + $this->coverpath = ltrim($this->coverpath,'/'); + } + } + } + + // set cover + if($path !== false){ + if (!$hascover) return; // TODO For now only update + + if($path){ + $item->attr('opf:media-type',$mime); + + // remember path for save action + $this->imagetoadd = $path; + } + + $this->reparse(); + } + + if (!$hascover) return $this->no_cover(); + + $zip = new ZipArchive(); + if(!@$zip->open($this->file)){ + throw new Exception('Failed to read epub file'); + } + $data = $zip->getFromName($this->coverpath); + + return array( + 'mime' => $mime, + 'data' => $data, + 'found' => $this->coverpath + ); + } /** * A simple getter/setter for simple meta attributes From 7bfa768f7c43d54e1d65863240bf937355bfbb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Thu, 31 Jan 2013 21:21:12 +0100 Subject: [PATCH 06/63] Fix epub save (hopefully) --- epub.php | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/epub.php b/epub.php index 8ff585a..857d29a 100644 --- a/epub.php +++ b/epub.php @@ -86,37 +86,21 @@ public function close (){ * 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=''; - } + $this->download (); $zip->close(); } /** * Get the updated epub */ - public function download($file){ + public function download($file=false){ $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->zip->FileReplace($this->coverpath,file_get_contents($this->imagetoadd)); $this->imagetoadd=''; } - $this->zip->Flush(TBSZIP_DOWNLOAD, $file); + if ($file) $this->zip->Flush(TBSZIP_DOWNLOAD, $file); } From 7d9e3aa80d382e53d6b0894410e2e5c5d98f739f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Tue, 5 Feb 2013 20:57:33 +0100 Subject: [PATCH 07/63] Refactor a little to prepare another method --- epub.php | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/epub.php b/epub.php index 857d29a..3d5f901 100644 --- a/epub.php +++ b/epub.php @@ -393,30 +393,29 @@ public function Cover($path=false, $mime=false){ ); } + public function getCoverItem () { + $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + if(!$nodes->length) return NULL; + + $coverid = (String) $nodes->item(0)->attr('opf:content'); + if(!$coverid) return NULL; + + $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]'); + if(!$nodes->length) return NULL; + + return $nodes->item(0); + } public function Cover2($path=false, $mime=false){ $hascover = true; - $item; - // load cover - $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); - if(!$nodes->length){ + $item = $this->getCoverItem (); + if (is_null ($item)) { $hascover = false; - } else{ - $coverid = (String) $nodes->item(0)->attr('opf:content'); - if(!$coverid){ - $hascover = false; - } else{ - $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]'); - if(!$nodes->length){ - $hascover = false; - } else{ - $item = $nodes->item(0); - $mime = $item->attr('opf:media-type'); - $this->coverpath = $item->attr('opf:href'); - $this->coverpath = dirname('/'.$this->meta).'/'.$this->coverpath; // image path is relative to meta file - $this->coverpath = ltrim($this->coverpath,'/'); - } - } + } else { + $mime = $item->attr('opf:media-type'); + $this->coverpath = $item->attr('opf:href'); + $this->coverpath = dirname('/'.$this->meta).'/'.$this->coverpath; // image path is relative to meta file + $this->coverpath = ltrim($this->coverpath,'/'); } // set cover From e31274fcd5e3553c20505b778c766a9277523337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Tue, 5 Feb 2013 20:58:58 +0100 Subject: [PATCH 08/63] Add a method to add another property to the epub to allow the cover to work with kepub.epub Warning : the epub won't be valid anymore. --- epub.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/epub.php b/epub.php index 3d5f901..721a3b3 100644 --- a/epub.php +++ b/epub.php @@ -406,6 +406,13 @@ public function getCoverItem () { return $nodes->item(0); } + public function updateForKepub () { + $item = $this->getCoverItem (); + if (!is_null ($item)) { + $item->attr('opf:properties', 'cover-image'); + } + } + public function Cover2($path=false, $mime=false){ $hascover = true; $item = $this->getCoverItem (); From f4b19e13e198858a79bb449b36dfba5413092d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Tue, 19 Mar 2013 21:06:56 +0100 Subject: [PATCH 09/63] Upgrade tbszip to latest release Fix updating zip without signature in header --- tbszip.php | 145 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 52 deletions(-) diff --git a/tbszip.php b/tbszip.php index ba1214d..fbbb8cb 100644 --- a/tbszip.php +++ b/tbszip.php @@ -1,7 +1,8 @@ CdPos = $this->CdInfo['p_cd']; } - function Open($ArchFile) { + function Open($ArchFile, $UseIncludePath=false) { // Open the zip archive if (!isset($this->Meth8Ok)) $this->__construct(); // for PHP 4 compatibility $this->Close(); // close handle and init info @@ -44,7 +45,7 @@ function Open($ArchFile) { $this->ArchFile = $ArchFile; $this->ArchIsNew = false; // open the file - $this->ArchHnd = fopen($ArchFile, 'rb'); + $this->ArchHnd = fopen($ArchFile, 'rb', $UseIncludePath); $ok = !($this->ArchHnd===false); if ($ok) $ok = $this->CentralDirRead(); return $ok; @@ -89,7 +90,7 @@ function CentralDirRead() { $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.'); + if ($b!==$cd_info) return $this->RaiseError('The End of Central Rirectory Record is not found.'); $this->CdEndPos = ftell($this->ArchHnd) - 4; $this->CdInfo = $this->CentralDirRead_End($cd_info); @@ -97,8 +98,8 @@ function CentralDirRead() { $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.'); + if ($this->CdFileNbr<=0) return $this->RaiseError('No header found in the Central Directory.'); + if ($this->CdPos<=0) return $this->RaiseError('No position found for the Central Directory.'); $this->_MoveTo($this->CdPos); for ($i=0;$i<$this->CdFileNbr;$i++) { @@ -114,13 +115,13 @@ function CentralDirRead() { 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['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['p_cd'] = $this->_GetDec($b,16,4); // position 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; @@ -131,7 +132,7 @@ 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.'.'); + if ($x!=='h:02014b50') return $this->RaiseError("Signature of Central Directory Header #".$idx." (file information) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd) - 46)."."); $x = array(); $x['vers_used'] = $this->_GetDec($b,4,2); @@ -169,36 +170,56 @@ 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"; + // Calculations first in order to have error messages before other information $idx = 0; $pos = 0; + $pos_stop = $this->CdInfo['p_cd']; $this->_MoveTo($pos); - while ($ok = $this->_ReadFile($idx,false)) { - $this->VisFileLst[$idx]['debug_pos'] = $pos; + while ( ($pos<$pos_stop) && ($ok = $this->_ReadFile($idx,false)) ) { + $this->VisFileLst[$idx]['p_this_header (debug_mode only)'] = $pos; $pos = ftell($this->ArchHnd); $idx++; } - print_r($this->VisFileLst); + } + + $nl = "\r\n"; + echo "
";
+		
+		echo "-------------------------------".$nl;
+		echo "End of Central Directory record".$nl;
+		echo "-------------------------------".$nl;
+		print_r($this->DebugArray($this->CdInfo));
+
+		echo $nl;
+		echo "-------------------------".$nl;
+		echo "Central Directory headers".$nl;
+		echo "-------------------------".$nl;
+		print_r($this->DebugArray($this->CdFileLst));
+
+		if ($FileHeaders) {
+			echo $nl;
+			echo "------------------".$nl;
+			echo "Local File headers".$nl;
+			echo "------------------".$nl;
+			print_r($this->DebugArray($this->VisFileLst));
 		}
 
+		echo "
"; + } + function DebugArray($arr) { + foreach ($arr as $k=>$v) { + if (is_array($v)) { + $arr[$k] = $this->DebugArray($v); + } elseif (substr($k,0,2)=='p_') { + $arr[$k] = $this->_TxtPos($v); + } + } + return $arr; + } + function FileExists($NameOrIdx) { return ($this->FileGetIdx($NameOrIdx)!==false); } @@ -272,9 +293,9 @@ 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.'.'); + if ($x!=='h:04034b50') return $this->RaiseError("Signature of Local File Header #".$idx." (data section) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd)-30)."."); $x = array(); $x['vers'] = $this->_GetDec($b,4,2); @@ -293,15 +314,20 @@ function _ReadFile($idx, $ReadData) { $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; + if (isset($this->CdFileLst[$idx])) { + $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."."; + } + } } 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 ($len==0) $this->RaiseError("File Data #".$idx." cannt be read because no length is specified in the Local File Header and its Central Directory information has not been found."); } if ($ReadData) { @@ -309,16 +335,25 @@ function _ReadFile($idx, $ReadData) { } 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); + $b = $this->_ReadData(12); + $s = $this->_GetHex($b,0,4); + $d = 0; + // the specification says the signature may or may not be present + if ($s=='h:08074b50') { + $b .= $this->_ReadData(4); + $d = 4; + $x['desc_bin'] = $b; + $x['desc_sign'] = $s; + } else { + $x['desc_bin'] = $b; + } + $x['desc_crc32'] = $this->_GetDec($b,0+$d,4); + $x['desc_l_data_c'] = $this->_GetDec($b,4+$d,4); + $x['desc_l_data_u'] = $this->_GetDec($b,8+$d,4); } // Save file info without the data @@ -441,9 +476,10 @@ function Flush($Render=TBSZIP_DOWNLOAD, $File='', $ContentType='') { 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 + $d = (strlen($b2)==16) ? 4 : 0; // offset because of the signature if any + $this->_PutDec($b2, $ReplInfo['crc32'], $d+0, 4); // crc32 + $this->_PutDec($b2, $ReplInfo['len_c'], $d+4, 4); // l_data_c + $this->_PutDec($b2, $ReplInfo['len_u'], $d+8, 4); // l_data_u } // output data $this->OutputFromString($b1.$ReplInfo['data'].$b2); @@ -501,7 +537,7 @@ function Flush($Render=TBSZIP_DOWNLOAD, $File='', $ContentType='') { $ArchPos += $old_cd_len; $DeltaCdLen = $DeltaCdLen + strlen($b2) - $old_cd_len; - // Output until Central Directory footer + // Output until "end of central directory record" 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 @@ -514,7 +550,7 @@ function Flush($Render=TBSZIP_DOWNLOAD, $File='', $ContentType='') { $DeltaCdLen += strlen($b2); } - // Output Central Directory footer + // Output "end of central directory record" $b2 = $this->CdInfo['bin']; $DelNbr = count($DelLst); if ( ($AddNbr>0) or ($DelNbr>0) ) { @@ -708,6 +744,11 @@ function _MsDos_Debug($date, $time) { 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 _TxtPos($pos) { + // Return the human readable position in both decimal and hexa + return $pos." (h:".dechex($pos).")"; + } + function _DataOuputAddedFile($Idx, $PosLoc) { $Ref =& $this->AddInfo[$Idx]; From 4493e3cd82355f8f296b12c8b50ae68e2366c488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Tue, 19 Mar 2013 21:08:41 +0100 Subject: [PATCH 10/63] Fix save method and a method to remove iTunes crap. --- epub.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/epub.php b/epub.php index 721a3b3..0b1fd2c 100644 --- a/epub.php +++ b/epub.php @@ -81,13 +81,21 @@ public function close (){ $this->zip->Close (); } + public function cleanITunesCrap () { + if ($this->zip->FileExists("iTunesMetadata.plist")) { + $this->zip->FileReplace ("iTunesMetadata.plist", false); + } + if ($this->zip->FileExists("iTunesArtwork")) { + $this->zip->FileReplace ("iTunesArtwork", false); + } + } + /** * Writes back all meta data changes - * TODO update */ public function save(){ $this->download (); - $zip->close(); + $this->zip->close(); } /** From d2d3e68bb21e6b2d14a504a85d7727540b4bfc3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Tue, 26 Mar 2013 20:16:37 +0100 Subject: [PATCH 11/63] Fix dirname output on Windows. Remove some leftover in Cover2. --- epub.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/epub.php b/epub.php index 0b1fd2c..26466b2 100644 --- a/epub.php +++ b/epub.php @@ -430,6 +430,7 @@ public function Cover2($path=false, $mime=false){ $mime = $item->attr('opf:media-type'); $this->coverpath = $item->attr('opf:href'); $this->coverpath = dirname('/'.$this->meta).'/'.$this->coverpath; // image path is relative to meta file + $this->coverpath = ltrim($this->coverpath,'\\'); $this->coverpath = ltrim($this->coverpath,'/'); } @@ -448,18 +449,6 @@ public function Cover2($path=false, $mime=false){ } if (!$hascover) return $this->no_cover(); - - $zip = new ZipArchive(); - if(!@$zip->open($this->file)){ - throw new Exception('Failed to read epub file'); - } - $data = $zip->getFromName($this->coverpath); - - return array( - 'mime' => $mime, - 'data' => $data, - 'found' => $this->coverpath - ); } /** From b24061ab2b878798dc6b046b1e7f73fa307b9274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Tue, 26 Mar 2013 21:06:02 +0100 Subject: [PATCH 12/63] Update licence and Author shoud have done that a long time ago. --- LICENSE | 3 +++ epub.php | 1 + 2 files changed, 4 insertions(+) diff --git a/LICENSE b/LICENSE index 128bf1f..2fcf6f9 100644 --- a/LICENSE +++ b/LICENSE @@ -17,3 +17,6 @@ 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. + +All code created or modified by Sébastien Lucas is licensed +under GPL 2 (http://www.gnu.org/licenses/gpl.html). \ No newline at end of file diff --git a/epub.php b/epub.php index 26466b2..c3927a5 100644 --- a/epub.php +++ b/epub.php @@ -3,6 +3,7 @@ * PHP EPub Meta library * * @author Andreas Gohr + * @author Sébastien Lucas */ require_once('tbszip.php'); From 1e349453b840cfe6af298c0b7076612dfdbe48a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Fri, 3 May 2013 11:49:54 +0200 Subject: [PATCH 13/63] Prepare the integration with Monocle. Heavily based on https://github.com/micahcraig/php-epub-meta . --- epub.php | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/epub.php b/epub.php index c3927a5..8ad923e 100644 --- a/epub.php +++ b/epub.php @@ -12,7 +12,9 @@ class EPub { public $xml; //FIXME change to protected, later + public $toc; protected $xpath; + protected $toc_xpath; protected $file; protected $meta; protected $zip; @@ -65,6 +67,26 @@ public function __construct($file){ $this->xml->formatOutput = true; $this->xpath = new EPubDOMXPath($this->xml); } + + public function initSpineComponent () + { + $spine = $this->xpath->query('//opf:spine')->item(0); + $tocid = $spine->getAttribute('toc'); + $tochref = $this->xpath->query("//opf:manifest/opf:item[@id='$tocid']")->item(0)->attr('href'); + $tocpath = dirname($this->meta).'/'.$tochref; + // read epub toc + if (!$this->zip->FileExists($tocpath)) { + throw new Exception ("Unable to find " . $tocpath); + } + + $data = $this->zip->FileRead($tocpath); + $this->toc = new DOMDocument(); + $this->toc->registerNodeClass('DOMElement','EPubDOMElement'); + $this->toc->loadXML($data); + $this->toc_xpath = new EPubDOMXPath($this->toc); + $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI); + $this->toc_xpath->registerNamespace('x', $rootNamespace); + } /** * file name getter @@ -82,6 +104,9 @@ public function close (){ $this->zip->Close (); } + /** + * Remove iTunes files + */ public function cleanITunesCrap () { if ($this->zip->FileExists("iTunesMetadata.plist")) { $this->zip->FileReplace ("iTunesMetadata.plist", false); @@ -111,7 +136,55 @@ public function download($file=false){ } if ($file) $this->zip->Flush(TBSZIP_DOWNLOAD, $file); } + + /** + * Get the components list as an array + */ + public function components(){ + $spine = array(); + $nodes = $this->xpath->query('//opf:spine/opf:itemref'); + foreach($nodes as $node){ + $idref = $node->getAttribute('idref'); + $spine[] = $this->xpath->query("//opf:manifest/opf:item[@id='$idref']")->item(0)->getAttribute('href'); + } + return $spine; + } + + /** + * Get the component content + */ + public function component($comp) { + $path = dirname($this->meta).'/'.$comp; + if (!$this->zip->FileExists($path)) { + throw new Exception ("Unable to find " . $path); + } + + $data = $this->zip->FileRead($path); + return $data; + } + /** + * Get the component content type + */ + public function componentContentType($comp) { + return $this->xpath->query("//opf:manifest/opf:item[@href='$comp']")->item(0)->getAttribute('media-type'); + } + + /** + * Get the Epub content (TOC) as an array + * + * For each chapter there is a "title" and a "src" + */ + public function contents(){ + $contents = array(); + $nodes = $this->toc_xpath->query('//x:ncx/x:navMap/x:navPoint'); + foreach($nodes as $node){ + $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue; + $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src'); + $contents[] = array("title" => $title, "src" => $src); + } + return $contents; + } /** From 8c8843e5ee9a350653742ef08c718de1970297d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Fri, 3 May 2013 13:35:24 +0200 Subject: [PATCH 14/63] Handle the case when the library is used inside a directory. --- epub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epub.php b/epub.php index 8ad923e..af96a61 100644 --- a/epub.php +++ b/epub.php @@ -6,7 +6,7 @@ * @author Sébastien Lucas */ -require_once('tbszip.php'); +require_once(realpath( dirname( __FILE__ ) ) . 'tbszip.php'); define ("METADATA_FILE", "META-INF/container.xml"); From 3ce1750555f4b9ec871996c0363c8209f1777daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Fri, 3 May 2013 13:50:05 +0200 Subject: [PATCH 15/63] Forgot a / --- epub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epub.php b/epub.php index af96a61..83563fb 100644 --- a/epub.php +++ b/epub.php @@ -6,7 +6,7 @@ * @author Sébastien Lucas */ -require_once(realpath( dirname( __FILE__ ) ) . 'tbszip.php'); +require_once(realpath( dirname( __FILE__ ) ) . '/tbszip.php'); define ("METADATA_FILE", "META-INF/container.xml"); From 9697706c717c73e71e6b0894634c1b284001eb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Wed, 23 Oct 2013 15:41:33 +0200 Subject: [PATCH 16/63] Update TbsZip version 2.15 (which handle zip with comments) --- tbszip.php | 113 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 17 deletions(-) diff --git a/tbszip.php b/tbszip.php index fbbb8cb..37db5ae 100644 --- a/tbszip.php +++ b/tbszip.php @@ -1,8 +1,8 @@ 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', $UseIncludePath); + $this->ArchIsStream = (is_resource($ArchFile) && (get_resource_type($ArchFile)=='stream')); + if ($this->ArchIsStream) { + $this->ArchFile = 'from_stream.zip'; + $this->ArchHnd = $ArchFile; + } else { + // open the file + $this->ArchFile = $ArchFile; + $this->ArchHnd = fopen($ArchFile, 'rb', $UseIncludePath); + } $ok = !($this->ArchHnd===false); if ($ok) $ok = $this->CentralDirRead(); return $ok; @@ -90,9 +96,18 @@ function CentralDirRead() { $cd_pos = -22; $this->_MoveTo($cd_pos, SEEK_END); $b = $this->_ReadData(4); - if ($b!==$cd_info) return $this->RaiseError('The End of Central Rirectory Record is not found.'); - - $this->CdEndPos = ftell($this->ArchHnd) - 4; + if ($b===$cd_info) { + $this->CdEndPos = ftell($this->ArchHnd) - 4; + } else { + $p = $this->_FindCDEnd($cd_info); + //echo 'p='.var_export($p,true); exit; + if ($p===false) { + return $this->RaiseError('The End of Central Directory Record is not found.'); + } else { + $this->CdEndPos = $p; + $this->_MoveTo($p+4); + } + } $this->CdInfo = $this->CentralDirRead_End($cd_info); $this->CdFileLst = array(); $this->CdFileNbr = $this->CdInfo['file_nbr_curr']; @@ -161,7 +176,13 @@ function CentralDirRead_File($idx) { } function RaiseError($Msg) { - if ($this->DisplayError) echo ''.get_class($this).' ERROR : '.$Msg.'
'."\r\n"; + if ($this->DisplayError) { + if (PHP_SAPI==='cli') { + echo get_class($this).' ERROR with the zip archive: '.$Msg."\r\n"; + } else { + echo ''.get_class($this).' ERROR with the zip archive: '.$Msg.'
'."\r\n"; + } + } $this->Error = $Msg; return false; } @@ -182,10 +203,10 @@ function Debug($FileHeaders=false) { $idx++; } } - + $nl = "\r\n"; echo "
";
-		
+
 		echo "-------------------------------".$nl;
 		echo "End of Central Directory record".$nl;
 		echo "-------------------------------".$nl;
@@ -206,7 +227,7 @@ function Debug($FileHeaders=false) {
 		}
 
 		echo "
"; - + } function DebugArray($arr) { @@ -219,7 +240,7 @@ function DebugArray($arr) { } return $arr; } - + function FileExists($NameOrIdx) { return ($this->FileGetIdx($NameOrIdx)!==false); } @@ -293,7 +314,7 @@ 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 Local File Header #".$idx." (data section) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd)-30)."."); @@ -335,7 +356,7 @@ function _ReadFile($idx, $ReadData) { } else { $this->_MoveTo($len, SEEK_CUR); } - + // Description information $desc_ok = ($x['purp'][2+3]=='1'); if ($desc_ok) { @@ -395,6 +416,32 @@ function FileReplace($NameOrIdx, $Data, $DataType=TBSZIP_STRING, $Compress=true) } + /** + * Return the state of the file. + * @return {string} 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown + */ + function FileGetState($NameOrIdx) { + + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx===false) { + $idx = $this->FileGetIdxAdd($NameOrIdx); + if ($idx===false) { + return false; + } else { + return 'a'; + } + } elseif (isset($this->ReplInfo[$idx])) { + if ($this->ReplInfo[$idx]===false) { + return 'd'; + } else { + return 'm'; + } + } else { + return 'u'; + } + + } + function FileCancelModif($NameOrIdx, $ReplacedAndDeleted=true) { // cancel added, modified or deleted modifications on a file in the archive // return the number of cancels @@ -585,8 +632,7 @@ function OutputOpen($Render, $File, $ContentType) { 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; + return $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.'); } } elseif (($Render & TBSZIP_STRING)==TBSZIP_STRING) { $this->OutputMode = TBSZIP_STRING; @@ -608,6 +654,8 @@ function OutputOpen($Render, $File, $ContentType) { $Len = $this->_EstimateNewArchSize(); if ($Len!==false) header('Content-Length: '.$Len); } + } else { + return $this->RaiseError('Method Flush is called with a unsupported render option.'); } return true; @@ -748,6 +796,34 @@ function _TxtPos($pos) { // Return the human readable position in both decimal and hexa return $pos." (h:".dechex($pos).")"; } + + /** + * Search the record of end of the Central Directory. + * Return the position of the record in the file. + * Return false if the record is not found. The comment cannot exceed 65335 bytes (=FFFF). + * The method is read backwards a block of 256 bytes and search the key in this block. + */ + function _FindCDEnd($cd_info) { + $nbr = 1; + $p = false; + $pos = ftell($this->ArchHnd) - 4 - 256; + while ( ($p===false) && ($nbr<256) ) { + if ($pos<=0) { + $pos = 0; + $nbr = 256; // in order to make this a last check + } + $this->_MoveTo($pos); + $x = $this->_ReadData(256); + $p = strpos($x, $cd_info); + if ($p===false) { + $nbr++; + $pos = $pos - 256 - 256; + } else { + return $pos + $p; + } + } + return false; + } function _DataOuputAddedFile($Idx, $PosLoc) { @@ -880,6 +956,9 @@ function _EstimateNewArchSize($Optim=true) { if ($this->ArchIsNew) { $Len = strlen($this->CdInfo['bin']); + } elseif ($this->ArchIsStream) { + $x = fstat($this->ArchHnd); + $Len = $x['size']; } else { $Len = filesize($this->ArchFile); } From 943bb09a91acd1fe45effb1dd26ea9f8a838af6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Wed, 23 Oct 2013 15:42:27 +0200 Subject: [PATCH 17/63] Fix a typo --- epub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epub.php b/epub.php index 83563fb..a8245d5 100644 --- a/epub.php +++ b/epub.php @@ -197,7 +197,7 @@ public function contents(){ * * array( * 'Pratchett, Terry' => 'Terry Pratchett', - * 'Simpson, Jacqeline' => 'Jacqueline Simpson', + * 'Simpson, Jacqueline' => 'Jacqueline Simpson', * ) * * @params array $authors From 586000e4289c4280a934902306b26ad59f01696b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Wed, 23 Oct 2013 16:16:30 +0200 Subject: [PATCH 18/63] Merge Marsender modifications --- epub.php | 105 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/epub.php b/epub.php index a8245d5..3714c4a 100644 --- a/epub.php +++ b/epub.php @@ -5,11 +5,11 @@ * @author Andreas Gohr * @author Sébastien Lucas */ - + require_once(realpath( dirname( __FILE__ ) ) . '/tbszip.php'); define ("METADATA_FILE", "META-INF/container.xml"); - + class EPub { public $xml; //FIXME change to protected, later public $toc; @@ -26,12 +26,13 @@ class EPub { * Constructor * * @param string $file path to epub file to work on + * @param string $zipClass class to handle zip * @throws Exception if metadata could not be loaded */ - public function __construct($file){ + public function __construct($file, $zipClass = 'clsTbsZip'){ // open file $this->file = $file; - $this->zip = new clsTbsZip(); + $this->zip = new $zipClass(); if(!$this->zip->Open($this->file)){ throw new Exception('Failed to read epub file'); } @@ -40,7 +41,7 @@ public function __construct($file){ 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'); @@ -56,7 +57,7 @@ public function __construct($file){ 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'); @@ -67,24 +68,24 @@ public function __construct($file){ $this->xml->formatOutput = true; $this->xpath = new EPubDOMXPath($this->xml); } - + public function initSpineComponent () { $spine = $this->xpath->query('//opf:spine')->item(0); $tocid = $spine->getAttribute('toc'); $tochref = $this->xpath->query("//opf:manifest/opf:item[@id='$tocid']")->item(0)->attr('href'); - $tocpath = dirname($this->meta).'/'.$tochref; + $tocpath = dirname($this->meta).'/'.$tochref; // read epub toc if (!$this->zip->FileExists($tocpath)) { throw new Exception ("Unable to find " . $tocpath); } - + $data = $this->zip->FileRead($tocpath); $this->toc = new DOMDocument(); $this->toc->registerNodeClass('DOMElement','EPubDOMElement'); $this->toc->loadXML($data); $this->toc_xpath = new EPubDOMXPath($this->toc); - $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI); + $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI); $this->toc_xpath->registerNamespace('x', $rootNamespace); } @@ -94,7 +95,7 @@ public function initSpineComponent () public function file(){ return $this->file; } - + /** * Close the epub file */ @@ -123,7 +124,7 @@ public function save(){ $this->download (); $this->zip->close(); } - + /** * Get the updated epub */ @@ -149,7 +150,7 @@ public function components(){ } return $spine; } - + /** * Get the component content */ @@ -158,11 +159,11 @@ public function component($comp) { if (!$this->zip->FileExists($path)) { throw new Exception ("Unable to find " . $path); } - + $data = $this->zip->FileRead($path); return $data; } - + /** * Get the component content type */ @@ -185,7 +186,6 @@ public function contents(){ } return $contents; } - /** * Get or set the book author(s) @@ -300,6 +300,61 @@ public function Description($description=false){ return $this->getset('dc:description',$description); } + /** + * Set or get the book's Unique Identifier + * + * @param string Unique identifier + */ + public function Uuid($uuid = false) + { + $nodes = $this->xpath->query('/opf:package'); + if ($nodes->length !== 1) { + $error = sprintf('Cannot find ebook identifier'); + throw new Exception($error); + } + $identifier = $nodes->item(0)->attr('unique-identifier'); + + $res = $this->getset('dc:identifier', $uuid, 'id', $identifier); + + return $res; + } + + /** + * Set or get the book's creation date + * + * @param string Date eg: 2012-05-19T12:54:25Z + */ + public function CreationDate($date = false) + { + $res = $this->getset('dc:date', $date, 'opf:event', 'creation'); + + return $res; + } + + /** + * Set or get the book's modification date + * + * @param string Date eg: 2012-05-19T12:54:25Z + */ + public function ModificationDate($date = false) + { + $res = $this->getset('dc:date', $date, 'opf:event', 'modification'); + + return $res; + } + + /** + * Set or get the book's URI + * + * @param string URI + */ + public function Uri($uri = false) + { + $res = $this->getset('dc:identifier', $uri, 'opf:scheme', 'URI'); + + return $res; + } + /** * Set or get the book's ISBN number * @@ -326,7 +381,7 @@ public function Google($google=false){ public function Amazon($amazon=false){ return $this->getset('dc:identifier',$amazon,'opf:scheme','AMAZON'); } - + /** * Set or get the Calibre UUID of the book * @@ -344,7 +399,7 @@ public function Calibre($uuid=false){ public function Serie($serie=false){ return $this->getset('opf:meta',$serie,'name','calibre:series','content'); } - + /** * Set or get the Serie Index of the book * @@ -353,7 +408,7 @@ public function Serie($serie=false){ 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) * @@ -474,11 +529,11 @@ public function Cover($path=false, $mime=false){ 'found' => $path ); } - + public function getCoverItem () { $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); if(!$nodes->length) return NULL; - + $coverid = (String) $nodes->item(0)->attr('opf:content'); if(!$coverid) return NULL; @@ -487,14 +542,14 @@ public function getCoverItem () { return $nodes->item(0); } - + public function updateForKepub () { $item = $this->getCoverItem (); if (!is_null ($item)) { $item->attr('opf:properties', 'cover-image'); } } - + public function Cover2($path=false, $mime=false){ $hascover = true; $item = $this->getCoverItem (); @@ -507,7 +562,7 @@ public function Cover2($path=false, $mime=false){ $this->coverpath = ltrim($this->coverpath,'\\'); $this->coverpath = ltrim($this->coverpath,'/'); } - + // set cover if($path !== false){ if (!$hascover) return; // TODO For now only update @@ -521,7 +576,7 @@ public function Cover2($path=false, $mime=false){ $this->reparse(); } - + if (!$hascover) return $this->no_cover(); } From 77f3790af4a034d2fb99cf2767ba85e8e1cfb16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Tue, 4 Mar 2014 17:04:14 +0100 Subject: [PATCH 19/63] Enable unit tests with Travis --- .travis.yml | 11 +++++++++++ test/test.phpunit.php | 46 ++++++++++++++++++++++--------------------- 2 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ea27500 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: php +php: + - 5.5 + - 5.4 + - 5.3 + - hhvm +script: + - phpunit test/test.phpunit.php +matrix: + allow_failures: + - php: hhvm diff --git a/test/test.phpunit.php b/test/test.phpunit.php index 88a9aa9..62dd7ab 100644 --- a/test/test.phpunit.php +++ b/test/test.phpunit.php @@ -1,6 +1,6 @@ epub = new EPub('test.copy.epub'); + $this->epub = new EPub(realpath( dirname( __FILE__ ) ) . '/test.copy.epub'); } - protected function tearDown(){ - unlink('test.copy.epub'); + public static function tearDownAfterClass() + { + unlink(realpath( dirname( __FILE__ ) ) . '/test.copy.epub'); } public function testAuthors(){ @@ -159,32 +160,33 @@ public function testSubject(){ } - public function testCover(){ + /*public function testCover(){ // read current cover - $cover = $this->epub->Cover(); + $cover = $this->epub->Cover2(); $this->assertEquals($cover['mime'],'image/png'); $this->assertEquals($cover['found'],'OPS/images/cover.png'); $this->assertEquals(strlen($cover['data']), 657911); - // delete cover - $cover = $this->epub->Cover(''); - $this->assertEquals($cover['mime'],'image/gif'); - $this->assertEquals($cover['found'],false); - $this->assertEquals(strlen($cover['data']), 42); + // // delete cover // Don't work anymore + // $cover = $this->epub->Cover(''); + // $this->assertEquals($cover['mime'],'image/gif'); + // $this->assertEquals($cover['found'],false); + // $this->assertEquals(strlen($cover['data']), 42); - // set new cover (will return a not-found as it's not yet saved) - $cover = $this->epub->Cover('test.jpg','image/jpeg'); - $this->assertEquals($cover['mime'],'image/jpeg'); - $this->assertEquals($cover['found'],'OPS/php-epub-meta-cover.img'); - $this->assertEquals(strlen($cover['data']), 0); + // // set new cover (will return a not-found as it's not yet saved) + $cover = $this->epub->Cover2(realpath( dirname( __FILE__ ) ) . '/test.jpg','image/jpeg'); + // $this->assertEquals($cover['mime'],'image/jpeg'); + // $this->assertEquals($cover['found'],'OPS/php-epub-meta-cover.img'); + // $this->assertEquals(strlen($cover['data']), 0); // save $this->epub->save(); + //$this->epub = new EPub(realpath( dirname( __FILE__ ) ) . '/test.copy.epub'); // read now changed cover - $cover = $this->epub->Cover(); + $cover = $this->epub->Cover2(); $this->assertEquals($cover['mime'],'image/jpeg'); - $this->assertEquals($cover['found'],'OPS/php-epub-meta-cover.img'); - $this->assertEquals(strlen($cover['data']), filesize('test.jpg')); - } + $this->assertEquals($cover['found'],'OPS/images/cover.png'); + $this->assertEquals(strlen($cover['data']), filesize(realpath( dirname( __FILE__ ) ) . '/test.jpg')); + }*/ } From 3c670c2ce14d28474abfbf53813bd026997e6612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Thu, 6 Mar 2014 10:29:54 +0100 Subject: [PATCH 20/63] Add a gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7522b49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +coverage/* +php-epub-meta.sublime-* From 8f6a8a90b0109123e99a046c3b187ac7f0619cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Thu, 6 Mar 2014 11:23:39 +0100 Subject: [PATCH 21/63] Handle code coverage --HG-- rename : test/test.phpunit.php => test/epubTest.php --- .gitignore | 1 + .travis.yml | 5 ++++- phpunit.xml | 20 ++++++++++++++++++++ test/{test.phpunit.php => epubTest.php} | 0 test/publishCoverage.sh | 17 +++++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 phpunit.xml rename test/{test.phpunit.php => epubTest.php} (100%) create mode 100644 test/publishCoverage.sh diff --git a/.gitignore b/.gitignore index 7522b49..685c5f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ coverage/* php-epub-meta.sublime-* +clover.xml diff --git a/.travis.yml b/.travis.yml index ea27500..7af60af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,10 @@ php: - 5.3 - hhvm script: - - phpunit test/test.phpunit.php + - phpunit +after_success: + - chmod +x test/publishCoverage.sh + - test/publishCoverage.sh matrix: allow_failures: - php: hhvm diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..aa7e489 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,20 @@ + + + + + ./ + + ./test + tbszip.php + + + + + + + + + test + + + \ No newline at end of file diff --git a/test/test.phpunit.php b/test/epubTest.php similarity index 100% rename from test/test.phpunit.php rename to test/epubTest.php diff --git a/test/publishCoverage.sh b/test/publishCoverage.sh new file mode 100644 index 0000000..463f7ed --- /dev/null +++ b/test/publishCoverage.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +PHP_VERSION=`php -v|grep --only-matching --perl-regexp "PHP 5\.\\d+"` +echo $PHP_VERSION + + +if [[ $PHP_VERSION != "PHP 5.4" ]] + then + echo "Bad PHP version" + exit +fi + +echo "Good PHP version" + +# Handle scrutinizer +wget https://scrutinizer-ci.com/ocular.phar +php ocular.phar code-coverage:upload --format=php-clover clover.xml From fd627ec7551839fbf7478de8e0d4a2051f58a647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Thu, 13 Mar 2014 20:44:49 +0100 Subject: [PATCH 22/63] Update epub.php --- epub.php | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 8 deletions(-) diff --git a/epub.php b/epub.php index 3714c4a..94abf9c 100644 --- a/epub.php +++ b/epub.php @@ -74,7 +74,7 @@ public function initSpineComponent () $spine = $this->xpath->query('//opf:spine')->item(0); $tocid = $spine->getAttribute('toc'); $tochref = $this->xpath->query("//opf:manifest/opf:item[@id='$tocid']")->item(0)->attr('href'); - $tocpath = dirname($this->meta).'/'.$tochref; + $tocpath = $this->getFullPath ($tochref); // read epub toc if (!$this->zip->FileExists($tocpath)) { throw new Exception ("Unable to find " . $tocpath); @@ -146,7 +146,7 @@ public function components(){ $nodes = $this->xpath->query('//opf:spine/opf:itemref'); foreach($nodes as $node){ $idref = $node->getAttribute('idref'); - $spine[] = $this->xpath->query("//opf:manifest/opf:item[@id='$idref']")->item(0)->getAttribute('href'); + $spine[] = $this->encodeComponentName ($this->xpath->query("//opf:manifest/opf:item[@id='$idref']")->item(0)->getAttribute('href')); } return $spine; } @@ -155,20 +155,71 @@ public function components(){ * Get the component content */ public function component($comp) { - $path = dirname($this->meta).'/'.$comp; + $path = $this->decodeComponentName ($comp); + $path = $this->getFullPath ($path); if (!$this->zip->FileExists($path)) { - throw new Exception ("Unable to find " . $path); + throw new Exception ("Unable to find {$path} <{$comp}>"); } $data = $this->zip->FileRead($path); return $data; } + public function getComponentName ($comp, $elementPath) { + $path = $this->decodeComponentName ($comp); + $path = $this->getFullPath ($path, $elementPath); + if (!$this->zip->FileExists($path)) { + error_log ("Unable to find " . $path); + return false; + } + $ref = dirname('/'.$this->meta); + $ref = ltrim($ref,'\\'); + $ref = ltrim($ref,'/'); + if (strlen ($ref) > 0) { + $path = str_replace ($ref . "/", "", $path); + } + return $this->encodeComponentName ($path); + } + + /** + * Encode the component name (to replace / and -) + */ + private function encodeComponentName ($src) { + return str_replace (array ("/", "-"), + array ("~SLASH~", "~DASH~"), + $src); + } + + /** + * Decode the component name (to replace / and -) + */ + private function decodeComponentName ($src) { + return str_replace (array ("~SLASH~", "~DASH~"), + array ("/", "-"), + $src); + } + + /** * Get the component content type */ public function componentContentType($comp) { - return $this->xpath->query("//opf:manifest/opf:item[@href='$comp']")->item(0)->getAttribute('media-type'); + $comp = $this->decodeComponentName ($comp); + $item = $this->xpath->query("//opf:manifest/opf:item[@href='$comp']")->item(0); + if ($item) return $item->getAttribute('media-type'); + + // I had at least one book containing %20 instead of spaces in the opf file + $comp = str_replace (" ", "%20", $comp); + $item = $this->xpath->query("//opf:manifest/opf:item[@href='$comp']")->item(0); + if ($item) return $item->getAttribute('media-type'); + return "application/octet-stream"; + } + + private function getNavPointDetail ($node) { + $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue; + $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src'); + $src = $this->decodeComponentName ($src); + return array("title" => $title, "src" => $src); } /** @@ -180,9 +231,12 @@ public function contents(){ $contents = array(); $nodes = $this->toc_xpath->query('//x:ncx/x:navMap/x:navPoint'); foreach($nodes as $node){ - $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue; - $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src'); - $contents[] = array("title" => $title, "src" => $src); + $contents[] = $this->getNavPointDetail ($node); + + $insidenodes = $this->toc_xpath->query('x:navPoint', $node); + foreach($insidenodes as $insidenode){ + $contents[] = $this->getNavPointDetail ($insidenode); + } } return $contents; } @@ -543,6 +597,53 @@ public function getCoverItem () { return $nodes->item(0); } + public function Combine($a, $b) + { + $isAbsolute = false; + if ($a[0] == "/") + $isAbsolute = true; + + if ($b[0] == "/") + throw new InvalidArgumentException("Second path part must not start with " . $m_Separator); + + $splittedA = preg_split("#/#", $a); + $splittedB = preg_split("#/#", $b); + + $pathParts = array(); + $mergedPath = array_merge($splittedA, $splittedB); + + foreach($mergedPath as $item) + { + if ($item == null || $item == "" || $item == ".") + continue; + + if ($item == "..") + { + array_pop($pathParts); + continue; + } + + array_push($pathParts, $item); + } + + $path = implode("/", $pathParts); + if ($isAbsolute) + return("/" . $path); + else + return($path); + } + + private function getFullPath ($file, $context = NULL) { + $path = dirname('/'.$this->meta).'/'.$file; + $path = ltrim($path,'\\'); + $path = ltrim($path,'/'); + if (!empty ($context)) { + $path = $this->combine (dirname ($path), $context); + } + //error_log ("FullPath : $path ($file / $context)"); + return $path; + } + public function updateForKepub () { $item = $this->getCoverItem (); if (!is_null ($item)) { From c23c2b550ebad54728dc26aefefa6dba80ca37f1 Mon Sep 17 00:00:00 2001 From: sb domo Date: Thu, 20 Mar 2014 08:11:56 +0100 Subject: [PATCH 23/63] Fix bug in epub navigation. --- epub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epub.php b/epub.php index 94abf9c..e4bd258 100644 --- a/epub.php +++ b/epub.php @@ -218,7 +218,7 @@ public function componentContentType($comp) { private function getNavPointDetail ($node) { $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue; $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src'); - $src = $this->decodeComponentName ($src); + $src = $this->encodeComponentName ($src); return array("title" => $title, "src" => $src); } From 71aaa24769d327999a6f1a5a6896b479c4211c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= Date: Fri, 19 Feb 2016 16:43:14 +0100 Subject: [PATCH 24/63] In case the filename contains non ASCII characters then follow rfc6266. This will fix the downloaded filename with Edge, IE11 if it contains a non ASCII character. --- tbszip.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tbszip.php b/tbszip.php index 37db5ae..ab75a44 100644 --- a/tbszip.php +++ b/tbszip.php @@ -645,7 +645,11 @@ function OutputOpen($Render, $File, $ContentType) { } else { header ('Pragma: no-cache'); if ($ContentType!='') header ('Content-Type: '.$ContentType); - header('Content-Disposition: attachment; filename="'.$File.'"'); + if (strlen($File) != strlen(utf8_decode($File))) { + header('Content-Disposition: attachment; filename="book.epub"; filename*=utf-8\'\'' . rawurlencode($File)); + } else { + header('Content-Disposition: attachment; filename="'.$File.'"'); + } header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Cache-Control: public'); From 5d40c59e7cfe402031f8435cfae4becc05bc8407 Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Thu, 30 Jun 2016 14:26:47 +0200 Subject: [PATCH 25/63] Add composer.json. --- composer.json | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6974f18 --- /dev/null +++ b/composer.json @@ -0,0 +1,49 @@ +{ + "name": "seblucas/php-epub-meta", + "type": "library", + "description": "Reading and writing metadata included in the EPub ebook format", + "keywords": ["epub", "metadata", "ebook"], + "homepage": "https://github.com/seblucas/php-epub-meta", + "authors": [ + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org", + "homepage": "https://www.splitbrain.org/", + "role": "Developer" + }, + { + "name": "SĂŠbastien Lucas", + "email": "sebastien@slucas.fr", + "homepage": "http://www.slucas.fr/", + "role": "Developer" + } + ], + "require": { + "php": ">=5.3.0", + "ext-xml": "*", + "ext-zip": "*", + "Skrol29/tbszip": "dev-master" + }, + "require-dev": { + }, + "autoload": { + "classmap": ["./"] + }, + "repositories": [ + { + "type": "package", + "package": { + "name": "Skrol29/tbszip", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Skrol29/tbszip", + "reference": "master" + }, + "autoload": { + "classmap": ["./"] + } + } + } + ] +} From ec1edb98381915de73399b0e5ec642829bfe79c2 Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Thu, 30 Jun 2016 15:32:35 +0200 Subject: [PATCH 26/63] Removed TbsZip and added instructions how to get this via Composer. --- README | 44 +++ epub.php | 2 - tbszip.php | 1007 ---------------------------------------------------- 3 files changed, 44 insertions(+), 1009 deletions(-) delete mode 100644 tbszip.php diff --git a/README b/README index 21927da..aa3018a 100644 --- a/README +++ b/README @@ -26,3 +26,47 @@ Using the "Lookup Book Data" link will open a dialog that searches the book at Google Books you can use the found data using the "fill in" and "replace" buttons. The former will only fill empty fields, while the latter will replace all data. Author filling is missing currently. + + +Installing via Composer +======================= + +You can use this package in your projects with [Composer](https://getcomposer.org/). Just +add these lines to your project's `composer.json`: + +``` + "require": { + "seblucas/php-epub-meta": "dev-master", + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/mbirth/php-epub-meta" + } + ] +``` + +Since this package requires TbsZip, you might have to add the following lines, too: + +``` + "require": { + "Skrol29/tbszip": "dev-master", + }, + "repositories": [ + { + "type": "package", + "package": { + "name": "Skrol29/tbszip", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Skrol29/tbszip", + "reference": "master" + }, + "autoload": { + "classmap": ["./"] + } + } + } + ] +``` diff --git a/epub.php b/epub.php index e4bd258..aac1736 100644 --- a/epub.php +++ b/epub.php @@ -6,8 +6,6 @@ * @author Sébastien Lucas */ -require_once(realpath( dirname( __FILE__ ) ) . '/tbszip.php'); - define ("METADATA_FILE", "META-INF/container.xml"); class EPub { diff --git a/tbszip.php b/tbszip.php deleted file mode 100644 index ab75a44..0000000 --- a/tbszip.php +++ /dev/null @@ -1,1007 +0,0 @@ -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, $UseIncludePath=false) { - // 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->ArchIsNew = false; - $this->ArchIsStream = (is_resource($ArchFile) && (get_resource_type($ArchFile)=='stream')); - if ($this->ArchIsStream) { - $this->ArchFile = 'from_stream.zip'; - $this->ArchHnd = $ArchFile; - } else { - // open the file - $this->ArchFile = $ArchFile; - $this->ArchHnd = fopen($ArchFile, 'rb', $UseIncludePath); - } - $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) { - $this->CdEndPos = ftell($this->ArchHnd) - 4; - } else { - $p = $this->_FindCDEnd($cd_info); - //echo 'p='.var_export($p,true); exit; - if ($p===false) { - return $this->RaiseError('The End of Central Directory Record is not found.'); - } else { - $this->CdEndPos = $p; - $this->_MoveTo($p+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 header found in the Central Directory.'); - if ($this->CdPos<=0) return $this->RaiseError('No position found for the Central Directory.'); - - $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); // position 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 Central Directory Header #".$idx." (file information) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd) - 46)."."); - - $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) { - if (PHP_SAPI==='cli') { - echo get_class($this).' ERROR with the zip archive: '.$Msg."\r\n"; - } else { - echo ''.get_class($this).' ERROR with the zip archive: '.$Msg.'
'."\r\n"; - } - } - $this->Error = $Msg; - return false; - } - - function Debug($FileHeaders=false) { - - $this->DisplayError = true; - - if ($FileHeaders) { - // Calculations first in order to have error messages before other information - $idx = 0; - $pos = 0; - $pos_stop = $this->CdInfo['p_cd']; - $this->_MoveTo($pos); - while ( ($pos<$pos_stop) && ($ok = $this->_ReadFile($idx,false)) ) { - $this->VisFileLst[$idx]['p_this_header (debug_mode only)'] = $pos; - $pos = ftell($this->ArchHnd); - $idx++; - } - } - - $nl = "\r\n"; - echo "
";
-
-		echo "-------------------------------".$nl;
-		echo "End of Central Directory record".$nl;
-		echo "-------------------------------".$nl;
-		print_r($this->DebugArray($this->CdInfo));
-
-		echo $nl;
-		echo "-------------------------".$nl;
-		echo "Central Directory headers".$nl;
-		echo "-------------------------".$nl;
-		print_r($this->DebugArray($this->CdFileLst));
-
-		if ($FileHeaders) {
-			echo $nl;
-			echo "------------------".$nl;
-			echo "Local File headers".$nl;
-			echo "------------------".$nl;
-			print_r($this->DebugArray($this->VisFileLst));
-		}
-
-		echo "
"; - - } - - function DebugArray($arr) { - foreach ($arr as $k=>$v) { - if (is_array($v)) { - $arr[$k] = $this->DebugArray($v); - } elseif (substr($k,0,2)=='p_') { - $arr[$k] = $this->_TxtPos($v); - } - } - return $arr; - } - - 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 Local File Header #".$idx." (data section) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd)-30)."."); - - $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 - if (isset($this->CdFileLst[$idx])) { - $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."."; - } - } - } else { - $len = $x['l_data_c']; - if ($len==0) $this->RaiseError("File Data #".$idx." cannt be read because no length is specified in the Local File Header and its Central Directory information has not been found."); - } - - 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(12); - $s = $this->_GetHex($b,0,4); - $d = 0; - // the specification says the signature may or may not be present - if ($s=='h:08074b50') { - $b .= $this->_ReadData(4); - $d = 4; - $x['desc_bin'] = $b; - $x['desc_sign'] = $s; - } else { - $x['desc_bin'] = $b; - } - $x['desc_crc32'] = $this->_GetDec($b,0+$d,4); - $x['desc_l_data_c'] = $this->_GetDec($b,4+$d,4); - $x['desc_l_data_u'] = $this->_GetDec($b,8+$d,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; - - } - - /** - * Return the state of the file. - * @return {string} 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown - */ - function FileGetState($NameOrIdx) { - - $idx = $this->FileGetIdx($NameOrIdx); - if ($idx===false) { - $idx = $this->FileGetIdxAdd($NameOrIdx); - if ($idx===false) { - return false; - } else { - return 'a'; - } - } elseif (isset($this->ReplInfo[$idx])) { - if ($this->ReplInfo[$idx]===false) { - return 'd'; - } else { - return 'm'; - } - } else { - return 'u'; - } - - } - - 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!=='') { - $d = (strlen($b2)==16) ? 4 : 0; // offset because of the signature if any - $this->_PutDec($b2, $ReplInfo['crc32'], $d+0, 4); // crc32 - $this->_PutDec($b2, $ReplInfo['len_c'], $d+4, 4); // l_data_c - $this->_PutDec($b2, $ReplInfo['len_u'], $d+8, 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 "end of central directory record" - 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 "end of central directory record" - $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) { - return $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.'); - } - } 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); - if (strlen($File) != strlen(utf8_decode($File))) { - header('Content-Disposition: attachment; filename="book.epub"; filename*=utf-8\'\'' . rawurlencode($File)); - } else { - 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); - } - } else { - return $this->RaiseError('Method Flush is called with a unsupported render option.'); - } - - 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 _TxtPos($pos) { - // Return the human readable position in both decimal and hexa - return $pos." (h:".dechex($pos).")"; - } - - /** - * Search the record of end of the Central Directory. - * Return the position of the record in the file. - * Return false if the record is not found. The comment cannot exceed 65335 bytes (=FFFF). - * The method is read backwards a block of 256 bytes and search the key in this block. - */ - function _FindCDEnd($cd_info) { - $nbr = 1; - $p = false; - $pos = ftell($this->ArchHnd) - 4 - 256; - while ( ($p===false) && ($nbr<256) ) { - if ($pos<=0) { - $pos = 0; - $nbr = 256; // in order to make this a last check - } - $this->_MoveTo($pos); - $x = $this->_ReadData(256); - $p = strpos($x, $cd_info); - if ($p===false) { - $nbr++; - $pos = $pos - 256 - 256; - } else { - return $pos + $p; - } - } - return false; - } - - 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']); - } elseif ($this->ArchIsStream) { - $x = fstat($this->ArchHnd); - $Len = $x['size']; - } 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 From e0b5f46bf491e0ef33ceecad1d1ddf4ccf5e91ee Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Thu, 30 Jun 2016 16:11:08 +0200 Subject: [PATCH 27/63] Moved class to lib/ folder. --- composer.json | 2 +- epub.php => lib/EPub.php | 0 phpunit.xml | 5 +++-- 3 files changed, 4 insertions(+), 3 deletions(-) rename epub.php => lib/EPub.php (100%) diff --git a/composer.json b/composer.json index 6974f18..a3d663f 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "require-dev": { }, "autoload": { - "classmap": ["./"] + "classmap": ["lib/"] }, "repositories": [ { diff --git a/epub.php b/lib/EPub.php similarity index 100% rename from epub.php rename to lib/EPub.php diff --git a/phpunit.xml b/phpunit.xml index aa7e489..835461a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,8 +1,9 @@ - + ./ + ./lib/ ./test tbszip.php @@ -17,4 +18,4 @@ test - \ No newline at end of file + From aad60bd93cf464ac9d5c2d401927016aceb9befc Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Thu, 30 Jun 2016 16:13:34 +0200 Subject: [PATCH 28/63] Split file into single classes. --- lib/EPub.php | 117 ----------------------------------------- lib/EPubDOMElement.php | 110 ++++++++++++++++++++++++++++++++++++++ lib/EPubDOMXPath.php | 19 +++++++ 3 files changed, 129 insertions(+), 117 deletions(-) create mode 100644 lib/EPubDOMElement.php create mode 100644 lib/EPubDOMXPath.php diff --git a/lib/EPub.php b/lib/EPub.php index aac1736..367f28f 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -769,120 +769,3 @@ protected function reparse() { $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/lib/EPubDOMElement.php b/lib/EPubDOMElement.php new file mode 100644 index 0000000..a39df6f --- /dev/null +++ b/lib/EPubDOMElement.php @@ -0,0 +1,110 @@ + + * @author Sébastien Lucas + */ + +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/lib/EPubDOMXPath.php b/lib/EPubDOMXPath.php new file mode 100644 index 0000000..75d231c --- /dev/null +++ b/lib/EPubDOMXPath.php @@ -0,0 +1,19 @@ + + * @author Sébastien Lucas + */ + +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); + } + } + } +} From 6b877db17c99bcedbd61d961b2c6f73f977aea66 Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Thu, 30 Jun 2016 16:18:11 +0200 Subject: [PATCH 29/63] Fix includes of index.php. --- index.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.php b/index.php index 57bb31b..7fd3939 100644 --- a/index.php +++ b/index.php @@ -12,10 +12,13 @@ exit; } - require('util.php'); + require dirname(__FILE__) . '/util.php'; // load epub data - require('epub.php'); + require dirname(__FILE__) . '/lib/EPubDOMElement.php'; + require dirname(__FILE__) . '/lib/EPubDOMXPath.php'; + require dirname(__FILE__) . '/lib/EPub.php'; + if(isset($_REQUEST['book'])){ try{ $book = $_REQUEST['book']; From f470c86196183d7eb2cd2936edf79860046e2a6c Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Fri, 1 Jul 2016 10:40:41 +0200 Subject: [PATCH 30/63] Code cleanup. --- README => README.md | 13 ++++--- index.php | 89 +++++++++++++++++++++++---------------------- util.php | 38 ++++++++++--------- 3 files changed, 74 insertions(+), 66 deletions(-) rename README => README.md (86%) diff --git a/README b/README.md similarity index 86% rename from README rename to README.md index aa3018a..32972a9 100644 --- a/README +++ b/README.md @@ -1,4 +1,5 @@ -====== PHP EPub Meta ====== +PHP EPub Meta +============= This project aims to create a PHP class for reading and writing metadata included in the EPub ebook format. @@ -9,7 +10,9 @@ Please see the issue tracker for what's missing. Forks and pull requests welcome. -===== About the EPub Manager Web Interface ===== + +About the EPub Manager Web Interface +------------------------------------ The manager expects your ebooks in a single flat directory (no subfolders). The location of that directory has to be configured at the top of the index.php file. @@ -17,10 +20,10 @@ location of that directory has to be configured at the top of the index.php file All the epubs need to be read- and writable by the webserver. The manager also makes some assumption on how the files should be named. The -format is: "-.epub". Commas will be replaced by __ and -spaces are replaced by _. +format is: `<Author file-as>-<Title>.epub`. Commas will be replaced by `__` and +spaces are replaced by `_`. -Note that the manager will RENAME your files to that form when saving. +Note that the manager will **RENAME** your files to that form when saving. Using the "Lookup Book Data" link will open a dialog that searches the book at Google Books you can use the found data using the "fill in" and "replace" diff --git a/index.php b/index.php index 7fd3939..49c41bd 100644 --- a/index.php +++ b/index.php @@ -2,13 +2,10 @@ // modify this to point to your book directory $bookdir = '/home/andi/Dropbox/ebooks/'; - - error_reporting(E_ALL ^ E_NOTICE); - // proxy google requests - if(isset($_GET['api'])){ + if (isset($_GET['api'])) { header('application/json; charset=UTF-8'); - echo file_get_contents('https://www.googleapis.com/books/v1/volumes?q='.rawurlencode($_GET['api']).'&maxResults=25&printType=books&projection=full'); + echo file_get_contents('https://www.googleapis.com/books/v1/volumes?q=' . rawurlencode($_GET['api']) . '&maxResults=25&printType=books&projection=full'); exit; } @@ -19,26 +16,26 @@ require dirname(__FILE__) . '/lib/EPubDOMXPath.php'; require dirname(__FILE__) . '/lib/EPub.php'; - if(isset($_REQUEST['book'])){ - try{ + if (isset($_REQUEST['book'])) { + try { $book = $_REQUEST['book']; - $book = str_replace('..','',$book); // no upper dirs, lowers might be supported later - $epub = new EPub($bookdir.$book.'.epub'); - }catch (Exception $e){ + $book = str_replace('..', '', $book); // no upper dirs, lowers might be supported later + $epub = new EPub($bookdir . $book . '.epub'); + } catch (Exception $e) { $error = $e->getMessage(); } } // return image data - if(isset($_REQUEST['img']) && isset($epub)){ + if (isset($_REQUEST['img']) && isset($epub)) { $img = $epub->Cover(); - header('Content-Type: '.$img['mime']); + header('Content-Type: ' . $img['mime']); echo $img['data']; exit; } // save epub data - if($_REQUEST['save'] && isset($epub)){ + if ($_REQUEST['save'] && isset($epub)) { $epub->Title($_POST['title']); $epub->Description($_POST['description']); $epub->Language($_POST['language']); @@ -48,10 +45,12 @@ $epub->Subjects($_POST['subjects']); $authors = array(); - foreach((array) $_POST['authorname'] as $num => $name){ - if($name){ + foreach ((array)$_POST['authorname'] as $num => $name) { + if ($name) { $as = $_POST['authoras'][$num]; - if(!$as) $as = $name; + if (!$as) { + $as = $name; + } $authors[$as] = $name; } } @@ -59,46 +58,50 @@ // handle image $cover = ''; - if(preg_match('/^https?:\/\//i',$_POST['coverurl'])){ + if (preg_match('/^https?:\/\//i', $_POST['coverurl'])) { $data = @file_get_contents($_POST['coverurl']); - if($data){ + if ($data) { $cover = tempnam(sys_get_temp_dir(), 'epubcover'); - file_put_contents($cover,$data); + file_put_contents($cover, $data); unset($data); } - }elseif(is_uploaded_file($_FILES['coverfile']['tmp_name'])){ + } elseif(is_uploaded_file($_FILES['coverfile']['tmp_name'])) { $cover = $_FILES['coverfile']['tmp_name']; } - if($cover){ + if ($cover) { $info = @getimagesize($cover); - if(preg_match('/^image\/(gif|jpe?g|png)$/',$info['mime'])){ - $epub->Cover($cover,$info['meta']); - }else{ - $error = "Not a valid image file".$cover; + if (preg_match('/^image\/(gif|jpe?g|png)$/', $info['mime'])) { + $epub->Cover($cover, $info['meta']); + } else { + $error = 'Not a valid image file' . $cover; } } // save the ebook - try{ + try { $epub->save(); - }catch(Exception $e){ + } catch (Exception $e) { $error = $e->getMessage(); } // clean up temporary cover file - if($cover) @unlink($cover); + if ($cover) { + @unlink($cover); + } // rename $author = array_shift(array_keys($epub->Authors())); $title = $epub->Title(); - $new = to_file($author.'-'.$title); - $new = $bookdir.$new.'.epub'; + $new = to_file($author . '-' . $title); + $new = $bookdir . $new . '.epub'; $old = $epub->file(); - if(realpath($new) != realpath($old)){ - if(!@rename($old,$new)) $new = $old; //rename failed, stay here + if (realpath($new) != realpath($old)) { + if (!@rename($old, $new)) { + $new = $old; //rename failed, stay here + } } - $go = basename($new,'.epub'); - header('Location: ?book='.rawurlencode($go)); + $go = basename($new, '.epub'); + header('Location: ?book=' . rawurlencode($go)); exit; } @@ -113,7 +116,7 @@ <link rel="stylesheet" type="text/css" href="assets/css/style.css" /> <script type="text/javascript"> - <?php if($error) echo "alert('".htmlspecialchars($error)."');";?> + <?php if($error) echo "alert('" . htmlspecialchars($error) . "');";?> </script> </head> <body> @@ -121,12 +124,12 @@ <div id="wrapper"> <ul id="booklist"> <?php - $list = glob($bookdir.'/*.epub'); - foreach($list as $book){ - $base = basename($book,'.epub'); + $list = glob($bookdir . '/*.epub'); + foreach ($list as $book) { + $base = basename($book, '.epub'); $name = book_output($base); - echo '<li '.($base == $_REQUEST['book'] ? 'class="active"' : '' ).'>'; - echo '<a href="?book='.htmlspecialchars($base).'">'.$name.'</a>'; + echo '<li ' . ($base == $_REQUEST['book'] ? 'class="active"' : '' ) . '>'; + echo '<a href="?book=' . htmlspecialchars($base) . '">' . $name . '</a>'; echo '</li>'; } ?> @@ -146,7 +149,7 @@ <td id="authors"> <?php $count = 0; - foreach($epub->Authors() as $as => $name){ + foreach ($epub->Authors() as $as => $name) { ?> <p> <input type="text" name="authorname[<?php echo $count?>]" value="<?php echo htmlspecialchars($name)?>" /> @@ -167,7 +170,7 @@ class="<?php $c = $epub->Cover(); echo ($c['found']?'hasimg':'noimg')?>" /> </tr> <tr> <th>Subjects</th> - <td><input type="text" name="subjects" value="<?php echo htmlspecialchars(join(', ',$epub->Subjects()))?>" /></td> + <td><input type="text" name="subjects" value="<?php echo htmlspecialchars(join(', ', $epub->Subjects()))?>" /></td> </tr> <tr> <th>Publisher</th> @@ -201,7 +204,7 @@ class="<?php $c = $epub->Cover(); echo ($c['found']?'hasimg':'noimg')?>" /> <p>View and edit epub books stored in <code><?php echo htmlspecialchars($bookdir)?></code>.</p> <div class="license"> - <p><?php echo str_replace("\n\n",'</p><p>',htmlspecialchars(file_get_contents('LICENSE'))) ?></p> + <p><?php echo str_replace("\n\n", '</p><p>', htmlspecialchars(file_get_contents('LICENSE'))) ?></p> </div> <?php endif; ?> diff --git a/util.php b/util.php index 75cb8a8..633fcb9 100644 --- a/util.php +++ b/util.php @@ -1,30 +1,32 @@ <?php -function to_file($input){ - $input = str_replace(' ','_',$input); - $input = str_replace('__','_',$input); - $input = str_replace(',_',',',$input); - $input = str_replace('_,',',',$input); - $input = str_replace('-_','-',$input); - $input = str_replace('_-','-',$input); - $input = str_replace(',','__',$input); +function to_file($input) +{ + $input = str_replace( ' ', '_', $input); + $input = str_replace('__', '_', $input); + $input = str_replace(',_', ',', $input); + $input = str_replace('_,', ',', $input); + $input = str_replace('-_', '-', $input); + $input = str_replace('_-', '-', $input); + $input = str_replace( ',', '__', $input); return $input; } -function book_output($input){ - $input = str_replace('__',',',$input); - $input = str_replace('_',' ',$input); - $input = str_replace(',',', ',$input); - $input = str_replace('-',' - ',$input); - list($author,$title) = explode('-',$input,2); +function book_output($input) +{ + $input = str_replace('__', ',', $input); + $input = str_replace( '_', ' ', $input); + $input = str_replace( ',', ', ', $input); + $input = str_replace( '-', ' - ', $input); + list($author, $title) = explode('-', $input, 2); $author = trim($author); $title = trim($title); - if(!$title){ - $title = $author; + if (!$title) { + $title = $author; $author = ''; } - return '<span class="title">'.htmlspecialchars($title).'</span>'. - '<span class="author">'.htmlspecialchars($author).'</author>'; + return '<span class="title">' . htmlspecialchars($title) . '</span>' . + '<span class="author">' . htmlspecialchars($author) . '</author>'; } From eecd3669379383e75ed9d06d430ac420f9091ee3 Mon Sep 17 00:00:00 2001 From: Markus Birth <mbirth@gmail.com> Date: Fri, 1 Jul 2016 11:45:30 +0200 Subject: [PATCH 31/63] Code cleanup. --- lib/EPub.php | 486 +++++++++++++++++++++++------------------ lib/EPubDOMElement.php | 83 +++---- lib/EPubDOMXPath.php | 15 +- 3 files changed, 332 insertions(+), 252 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index 367f28f..f0e5979 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -3,13 +3,14 @@ * PHP EPub Meta library * * @author Andreas Gohr <andi@splitbrain.org> - * @author Sébastien Lucas <sebastien@slucas.fr> + * @author SĂŠbastien Lucas <sebastien@slucas.fr> */ -define ("METADATA_FILE", "META-INF/container.xml"); +define('METADATA_FILE', 'META-INF/container.xml'); -class EPub { - public $xml; //FIXME change to protected, later +class EPub +{ + public $xml; //FIXME: change to protected, later public $toc; protected $xpath; protected $toc_xpath; @@ -27,25 +28,26 @@ class EPub { * @param string $zipClass class to handle zip * @throws Exception if metadata could not be loaded */ - public function __construct($file, $zipClass = 'clsTbsZip'){ + public function __construct($file, $zipClass = 'clsTbsZip') + { // open file $this->file = $file; $this->zip = new $zipClass(); - if(!$this->zip->Open($this->file)){ + 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"); + throw new Exception('Unable to find metadata.xml'); } $data = $this->zip->FileRead(METADATA_FILE); - if($data == false){ + if ($data == false) { throw new Exception('Failed to access epub container data'); } $xml = new DOMDocument(); - $xml->registerNodeClass('DOMElement','EPubDOMElement'); + $xml->registerNodeClass('DOMElement', 'EPubDOMElement'); $xml->loadXML($data); $xpath = new EPubDOMXPath($xml); $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]'); @@ -53,34 +55,34 @@ public function __construct($file, $zipClass = 'clsTbsZip'){ // load metadata if (!$this->zip->FileExists($this->meta)) { - throw new Exception ("Unable to find " . $this->meta); + throw new Exception('Unable to find ' . $this->meta); } $data = $this->zip->FileRead($this->meta); - if(!$data){ + if (!$data) { throw new Exception('Failed to access epub metadata'); } $this->xml = new DOMDocument(); - $this->xml->registerNodeClass('DOMElement','EPubDOMElement'); + $this->xml->registerNodeClass('DOMElement', 'EPubDOMElement'); $this->xml->loadXML($data); $this->xml->formatOutput = true; $this->xpath = new EPubDOMXPath($this->xml); } - public function initSpineComponent () + public function initSpineComponent() { $spine = $this->xpath->query('//opf:spine')->item(0); $tocid = $spine->getAttribute('toc'); - $tochref = $this->xpath->query("//opf:manifest/opf:item[@id='$tocid']")->item(0)->attr('href'); - $tocpath = $this->getFullPath ($tochref); + $tochref = $this->xpath->query('//opf:manifest/opf:item[@id="' . $tocid . '"]')->item(0)->attr('href'); + $tocpath = $this->getFullPath($tochref); // read epub toc if (!$this->zip->FileExists($tocpath)) { - throw new Exception ("Unable to find " . $tocpath); + throw new Exception('Unable to find ' . $tocpath); } $data = $this->zip->FileRead($tocpath); - $this->toc = new DOMDocument(); - $this->toc->registerNodeClass('DOMElement','EPubDOMElement'); + $this->toc = new DOMDocument(); + $this->toc->registerNodeClass('DOMElement', 'EPubDOMElement'); $this->toc->loadXML($data); $this->toc_xpath = new EPubDOMXPath($this->toc); $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI); @@ -90,61 +92,69 @@ public function initSpineComponent () /** * file name getter */ - public function file(){ + public function file() + { return $this->file; } /** * Close the epub file */ - public function close (){ + public function close() + { $this->zip->FileCancelModif($this->meta); - // TODO : Add cancelation of cover image - $this->zip->Close (); + // TODO: Add cancelation of cover image + $this->zip->Close(); } /** * Remove iTunes files */ - public function cleanITunesCrap () { - if ($this->zip->FileExists("iTunesMetadata.plist")) { - $this->zip->FileReplace ("iTunesMetadata.plist", false); + public function cleanITunesCrap() + { + if ($this->zip->FileExists('iTunesMetadata.plist')) { + $this->zip->FileReplace('iTunesMetadata.plist', false); } - if ($this->zip->FileExists("iTunesArtwork")) { - $this->zip->FileReplace ("iTunesArtwork", false); + if ($this->zip->FileExists('iTunesArtwork')) { + $this->zip->FileReplace('iTunesArtwork', false); } } /** * Writes back all meta data changes */ - public function save(){ - $this->download (); + public function save() + { + $this->download(); $this->zip->close(); } /** * Get the updated epub */ - public function download($file=false){ - $this->zip->FileReplace($this->meta,$this->xml->saveXML()); + public function download($file=false) + { + $this->zip->FileReplace($this->meta, $this->xml->saveXML()); // add the cover image - if($this->imagetoadd){ - $this->zip->FileReplace($this->coverpath,file_get_contents($this->imagetoadd)); + if ($this->imagetoadd) { + $this->zip->FileReplace($this->coverpath, file_get_contents($this->imagetoadd)); $this->imagetoadd=''; } - if ($file) $this->zip->Flush(TBSZIP_DOWNLOAD, $file); + if ($file) { + $this->zip->Flush(TBSZIP_DOWNLOAD, $file); + } } /** * Get the components list as an array */ - public function components(){ + public function components() + { $spine = array(); $nodes = $this->xpath->query('//opf:spine/opf:itemref'); - foreach($nodes as $node){ + foreach ($nodes as $node) { $idref = $node->getAttribute('idref'); - $spine[] = $this->encodeComponentName ($this->xpath->query("//opf:manifest/opf:item[@id='$idref']")->item(0)->getAttribute('href')); + $spine[] = $this->encodeComponentName($this->xpath->query('//opf:manifest/opf:item[@id="' . $idref . '"]')->item(0)->getAttribute('href')); } return $spine; } @@ -152,72 +162,82 @@ public function components(){ /** * Get the component content */ - public function component($comp) { - $path = $this->decodeComponentName ($comp); - $path = $this->getFullPath ($path); + public function component($comp) + { + $path = $this->decodeComponentName($comp); + $path = $this->getFullPath($path); if (!$this->zip->FileExists($path)) { - throw new Exception ("Unable to find {$path} <{$comp}>"); + throw new Exception('Unable to find ' . $path . ' <' . $comp . '>'); } $data = $this->zip->FileRead($path); return $data; } - public function getComponentName ($comp, $elementPath) { - $path = $this->decodeComponentName ($comp); - $path = $this->getFullPath ($path, $elementPath); + public function getComponentName($comp, $elementPath) + { + $path = $this->decodeComponentName($comp); + $path = $this->getFullPath($path, $elementPath); if (!$this->zip->FileExists($path)) { - error_log ("Unable to find " . $path); + error_log('Unable to find ' . $path); return false; } - $ref = dirname('/'.$this->meta); - $ref = ltrim($ref,'\\'); - $ref = ltrim($ref,'/'); - if (strlen ($ref) > 0) { - $path = str_replace ($ref . "/", "", $path); + $ref = dirname('/' . $this->meta); + $ref = ltrim($ref, '\\'); + $ref = ltrim($ref, '/'); + if (strlen($ref) > 0) { + $path = str_replace($ref . '/', '', $path); } - return $this->encodeComponentName ($path); + return $this->encodeComponentName($path); } /** * Encode the component name (to replace / and -) */ - private function encodeComponentName ($src) { - return str_replace (array ("/", "-"), - array ("~SLASH~", "~DASH~"), - $src); + private function encodeComponentName($src) + { + return str_replace(array('/', '-'), + array('~SLASH~', '~DASH~'), + $src); } /** * Decode the component name (to replace / and -) */ - private function decodeComponentName ($src) { - return str_replace (array ("~SLASH~", "~DASH~"), - array ("/", "-"), - $src); + private function decodeComponentName($src) + { + return str_replace(array('~SLASH~', '~DASH~'), + array('/', '-'), + $src); } /** * Get the component content type */ - public function componentContentType($comp) { - $comp = $this->decodeComponentName ($comp); - $item = $this->xpath->query("//opf:manifest/opf:item[@href='$comp']")->item(0); - if ($item) return $item->getAttribute('media-type'); + public function componentContentType($comp) + { + $comp = $this->decodeComponentName($comp); + $item = $this->xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); + if ($item) { + return $item->getAttribute('media-type'); + } // I had at least one book containing %20 instead of spaces in the opf file - $comp = str_replace (" ", "%20", $comp); - $item = $this->xpath->query("//opf:manifest/opf:item[@href='$comp']")->item(0); - if ($item) return $item->getAttribute('media-type'); - return "application/octet-stream"; + $comp = str_replace(' ', '%20', $comp); + $item = $this->xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); + if ($item) { + return $item->getAttribute('media-type'); + } + return 'application/octet-stream'; } - private function getNavPointDetail ($node) { + private function getNavPointDetail($node) + { $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue; $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src'); $src = $this->encodeComponentName ($src); - return array("title" => $title, "src" => $src); + return array('title' => $title, 'src' => $src); } /** @@ -225,15 +245,16 @@ private function getNavPointDetail ($node) { * * For each chapter there is a "title" and a "src" */ - public function contents(){ + public function contents() + { $contents = array(); $nodes = $this->toc_xpath->query('//x:ncx/x:navMap/x:navPoint'); - foreach($nodes as $node){ - $contents[] = $this->getNavPointDetail ($node); + foreach ($nodes as $node) { + $contents[] = $this->getNavPointDetail($node); $insidenodes = $this->toc_xpath->query('x:navPoint', $node); - foreach($insidenodes as $insidenode){ - $contents[] = $this->getNavPointDetail ($insidenode); + foreach ($insidenodes as $insidenode) { + $contents[] = $this->getNavPointDetail($insidenode); } } return $contents; @@ -254,28 +275,33 @@ public function contents(){ * * @params array $authors */ - public function Authors($authors=false){ + public function Authors($authors=false) + { // set new data - if($authors !== false){ + if ($authors !== false) { // Author where given as a comma separated list - if(is_string($authors)){ - if($authors == ''){ + if (is_string($authors)) { + if ($authors == '') { $authors = array(); - }else{ - $authors = explode(',',$authors); - $authors = array_map('trim',$authors); + } 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(); + 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); + 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); } @@ -287,20 +313,20 @@ public function Authors($authors=false){ $rolefix = false; $authors = array(); $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); - if($nodes->length == 0){ + 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){ + foreach ($nodes as $node) { $name = $node->nodeValue; $as = $node->attr('opf:file-as'); - if(!$as){ + if (!$as) { $as = $name; - $node->attr('opf:file-as',$as); + $node->attr('opf:file-as', $as); } - if($rolefix){ - $node->attr('opf:role','aut'); + if ($rolefix) { + $node->attr('opf:role', 'aut'); } $authors[$as] = $name; } @@ -312,8 +338,9 @@ public function Authors($authors=false){ * * @param string $title */ - public function Title($title=false){ - return $this->getset('dc:title',$title); + public function Title($title=false) + { + return $this->getset('dc:title', $title); } /** @@ -321,8 +348,9 @@ public function Title($title=false){ * * @param string $lang */ - public function Language($lang=false){ - return $this->getset('dc:language',$lang); + public function Language($lang=false) + { + return $this->getset('dc:language', $lang); } /** @@ -330,8 +358,9 @@ public function Language($lang=false){ * * @param string $publisher */ - public function Publisher($publisher=false){ - return $this->getset('dc:publisher',$publisher); + public function Publisher($publisher=false) + { + return $this->getset('dc:publisher', $publisher); } /** @@ -339,8 +368,9 @@ public function Publisher($publisher=false){ * * @param string $rights */ - public function Copyright($rights=false){ - return $this->getset('dc:rights',$rights); + public function Copyright($rights=false) + { + return $this->getset('dc:rights', $rights); } /** @@ -348,8 +378,9 @@ public function Copyright($rights=false){ * * @param string $description */ - public function Description($description=false){ - return $this->getset('dc:description',$description); + public function Description($description=false) + { + return $this->getset('dc:description', $description); } /** @@ -412,8 +443,9 @@ public function Uri($uri = false) * * @param string $isbn */ - public function ISBN($isbn=false){ - return $this->getset('dc:identifier',$isbn,'opf:scheme','ISBN'); + public function ISBN($isbn=false) + { + return $this->getset('dc:identifier', $isbn, 'opf:scheme', 'ISBN'); } /** @@ -421,8 +453,9 @@ public function ISBN($isbn=false){ * * @param string $google */ - public function Google($google=false){ - return $this->getset('dc:identifier',$google,'opf:scheme','GOOGLE'); + public function Google($google=false) + { + return $this->getset('dc:identifier', $google, 'opf:scheme', 'GOOGLE'); } /** @@ -430,8 +463,9 @@ public function Google($google=false){ * * @param string $amazon */ - public function Amazon($amazon=false){ - return $this->getset('dc:identifier',$amazon,'opf:scheme','AMAZON'); + public function Amazon($amazon=false) + { + return $this->getset('dc:identifier', $amazon, 'opf:scheme', 'AMAZON'); } /** @@ -439,8 +473,9 @@ public function Amazon($amazon=false){ * * @param string $uuid */ - public function Calibre($uuid=false){ - return $this->getset('dc:identifier',$uuid,'opf:scheme','calibre'); + public function Calibre($uuid=false) + { + return $this->getset('dc:identifier', $uuid, 'opf:scheme', 'calibre'); } /** @@ -448,8 +483,9 @@ public function Calibre($uuid=false){ * * @param string $serie */ - public function Serie($serie=false){ - return $this->getset('opf:meta',$serie,'name','calibre:series','content'); + public function Serie($serie=false) + { + return $this->getset('opf:meta', $serie, 'name', 'calibre:series', 'content'); } /** @@ -457,8 +493,9 @@ public function Serie($serie=false){ * * @param string $serieIndex */ - public function SerieIndex($serieIndex=false){ - return $this->getset('opf:meta',$serieIndex,'name','calibre:series_index','content'); + public function SerieIndex($serieIndex=false) + { + return $this->getset('opf:meta', $serieIndex, 'name', 'calibre:series_index', 'content'); } /** @@ -469,27 +506,28 @@ public function SerieIndex($serieIndex=false){ * * @param array $subjects */ - public function Subjects($subjects=false){ + public function Subjects($subjects=false) + { // setter - if($subjects !== false){ - if(is_string($subjects)){ - if($subjects === ''){ + if ($subjects !== false) { + if (is_string($subjects)) { + if ($subjects === '') { $subjects = array(); - }else{ - $subjects = explode(',',$subjects); - $subjects = array_map('trim',$subjects); + } else { + $subjects = explode(',', $subjects); + $subjects = array_map('trim', $subjects); } } // delete previous $nodes = $this->xpath->query('//opf:metadata/dc:subject'); - foreach($nodes as $node){ + 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)); + foreach ($subjects as $subj) { + $node = $this->xml->createElement('dc:subject', htmlspecialchars($subj)); $node = $parent->appendChild($node); } @@ -499,7 +537,7 @@ public function Subjects($subjects=false){ //getter $subjects = array(); $nodes = $this->xpath->query('//opf:metadata/dc:subject'); - foreach($nodes as $node){ + foreach ($nodes as $node) { $subjects[] = $node->nodeValue; } return $subjects; @@ -525,29 +563,34 @@ public function Subjects($subjects=false){ * @param string $mime mime type of the given file * @return array */ - public function Cover($path=false, $mime=false){ + public function Cover($path=false, $mime=false) + { // set cover - if($path !== false){ + if ($path !== false) { // remove current pointer $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); - foreach($nodes as $node) $node->delete(); + 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(); + foreach ($nodes as $node) { + $node->delete(); + } - if($path){ + 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'); + $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); + $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; @@ -558,19 +601,25 @@ public function Cover($path=false, $mime=false){ // load cover $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); - if(!$nodes->length) return $this->no_cover(); + if (!$nodes->length) { + return $this->no_cover(); + } $coverid = (String) $nodes->item(0)->attr('opf:content'); - if(!$coverid) return $this->no_cover(); + if (!$coverid) { + return $this->no_cover(); + } - $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]'); - if(!$nodes->length) 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,'/'); + $path = dirname('/' . $this->meta) . '/' . $path; // image path is relative to meta file + $path = ltrim($path, '/'); $zip = new ZipArchive(); - if(!@$zip->open($this->file)){ + if (!@$zip->open($this->file)) { throw new Exception('Failed to read epub file'); } $data = $zip->getFromName($path); @@ -582,15 +631,22 @@ public function Cover($path=false, $mime=false){ ); } - public function getCoverItem () { + public function getCoverItem() + { $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); - if(!$nodes->length) return NULL; + if (!$nodes->length) { + return NULL; + } - $coverid = (String) $nodes->item(0)->attr('opf:content'); - if(!$coverid) return NULL; + $coverid = (String)$nodes->item(0)->attr('opf:content'); + if (!$coverid) { + return NULL; + } - $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]'); - if(!$nodes->length) return NULL; + $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]'); + if (!$nodes->length) { + return NULL; + } return $nodes->item(0); } @@ -598,25 +654,26 @@ public function getCoverItem () { public function Combine($a, $b) { $isAbsolute = false; - if ($a[0] == "/") + if ($a[0] == '/') { $isAbsolute = true; + } - if ($b[0] == "/") - throw new InvalidArgumentException("Second path part must not start with " . $m_Separator); + if ($b[0] == '/') { + throw new InvalidArgumentException('Second path part must not start with ' . $m_Separator); + } - $splittedA = preg_split("#/#", $a); - $splittedB = preg_split("#/#", $b); + $splittedA = preg_split('#/#', $a); + $splittedB = preg_split('#/#', $b); $pathParts = array(); $mergedPath = array_merge($splittedA, $splittedB); - foreach($mergedPath as $item) - { - if ($item == null || $item == "" || $item == ".") + foreach ($mergedPath as $item) { + if ($item == null || $item == '' || $item == '.') { continue; + } - if ($item == "..") - { + if ($item == '..') { array_pop($pathParts); continue; } @@ -624,50 +681,56 @@ public function Combine($a, $b) array_push($pathParts, $item); } - $path = implode("/", $pathParts); - if ($isAbsolute) - return("/" . $path); - else + $path = implode('/', $pathParts); + if ($isAbsolute) { + return('/' . $path); + } else { return($path); + } } - private function getFullPath ($file, $context = NULL) { - $path = dirname('/'.$this->meta).'/'.$file; - $path = ltrim($path,'\\'); - $path = ltrim($path,'/'); - if (!empty ($context)) { - $path = $this->combine (dirname ($path), $context); + private function getFullPath($file, $context = NULL) + { + $path = dirname('/' . $this->meta) . '/' . $file; + $path = ltrim($path, '\\'); + $path = ltrim($path, '/'); + if (!empty($context)) { + $path = $this->combine(dirname($path), $context); } //error_log ("FullPath : $path ($file / $context)"); return $path; } - public function updateForKepub () { - $item = $this->getCoverItem (); - if (!is_null ($item)) { + public function updateForKepub() + { + $item = $this->getCoverItem(); + if (!is_null($item)) { $item->attr('opf:properties', 'cover-image'); } } - public function Cover2($path=false, $mime=false){ + public function Cover2($path=false, $mime=false) + { $hascover = true; - $item = $this->getCoverItem (); - if (is_null ($item)) { + $item = $this->getCoverItem(); + if (is_null($item)) { $hascover = false; } else { $mime = $item->attr('opf:media-type'); $this->coverpath = $item->attr('opf:href'); - $this->coverpath = dirname('/'.$this->meta).'/'.$this->coverpath; // image path is relative to meta file - $this->coverpath = ltrim($this->coverpath,'\\'); - $this->coverpath = ltrim($this->coverpath,'/'); + $this->coverpath = dirname('/' . $this->meta) . '/' . $this->coverpath; // image path is relative to meta file + $this->coverpath = ltrim($this->coverpath, '\\'); + $this->coverpath = ltrim($this->coverpath, '/'); } // set cover - if($path !== false){ - if (!$hascover) return; // TODO For now only update + if ($path !== false) { + if (!$hascover) { + return; // TODO For now only update + } - if($path){ - $item->attr('opf:media-type',$mime); + if ($path) { + $item->attr('opf:media-type', $mime); // remember path for save action $this->imagetoadd = $path; @@ -676,7 +739,9 @@ public function Cover2($path=false, $mime=false){ $this->reparse(); } - if (!$hascover) return $this->no_cover(); + if (!$hascover) { + return $this->no_cover(); + } } /** @@ -690,42 +755,47 @@ public function Cover2($path=false, $mime=false){ * @param string $aval Attribute value * @param string $datt Destination attribute */ - protected function getset($item,$value=false,$att=false,$aval=false,$datt=false){ + protected function getset($item, $value=false, $att=false, $aval=false, $datt=false) + { // construct xpath - $xpath = '//opf:metadata/'.$item; - if($att){ - $xpath .= "[@$att=\"$aval\"]"; + $xpath = '//opf:metadata/' . $item; + if ($att) { + $xpath .= '[@' . $att . '="' . $aval . '"]'; } // set value - if($value !== false){ + if ($value !== false) { $value = htmlspecialchars($value); $nodes = $this->xpath->query($xpath); - if($nodes->length == 1 ){ - if($value === ''){ + if ($nodes->length == 1 ) { + if ($value === '') { // the user want's to empty this value -> delete the node $nodes->item(0)->delete(); - }else{ + } else { // replace value - if ($datt){ - $nodes->item(0)->attr ($datt, $value); - }else{ + if ($datt) { + $nodes->item(0)->attr($datt, $value); + } else { $nodes->item(0)->nodeValue = $value; } } - }else{ + } 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(); + foreach ($nodes as $n) { + $n->delete(); + } // readd them - if($value){ + 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 = $parent->newChild($item); + if ($att) { + $node->attr($att, $aval); + } + if ($datt) { + $node->attr($datt, $value); + } else { $node->nodeValue = $value; } } @@ -736,13 +806,13 @@ protected function getset($item,$value=false,$att=false,$aval=false,$datt=false) // get value $nodes = $this->xpath->query($xpath); - if($nodes->length){ - if ($datt){ - return $nodes->item(0)->attr ($datt); - }else{ + if ($nodes->length) { + if ($datt) { + return $nodes->item(0)->attr($datt); + } else { return $nodes->item(0)->nodeValue; } - }else{ + } else { return ''; } } @@ -750,7 +820,8 @@ protected function getset($item,$value=false,$att=false,$aval=false,$datt=false) /** * Return a not found response for Cover() */ - protected function no_cover(){ + protected function no_cover() + { return array( 'data' => base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'), 'mime' => 'image/gif', @@ -764,7 +835,8 @@ protected function no_cover(){ * I had to rely on this because otherwise xpath failed to find the newly * added nodes */ - protected function reparse() { + protected function reparse() + { $this->xml->loadXML($this->xml->saveXML()); $this->xpath = new EPubDOMXPath($this->xml); } diff --git a/lib/EPubDOMElement.php b/lib/EPubDOMElement.php index a39df6f..976ca85 100644 --- a/lib/EPubDOMElement.php +++ b/lib/EPubDOMElement.php @@ -3,44 +3,45 @@ * PHP EPub Meta library * * @author Andreas Gohr <andi@splitbrain.org> - * @author Sébastien Lucas <sebastien@slucas.fr> + * @author SĂŠbastien Lucas <sebastien@slucas.fr> */ -class EPubDOMElement extends DOMElement { +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/' + 'dc' => 'http://purl.org/dc/elements/1.1/', ); - - public function __construct($name, $value='', $namespaceURI=''){ - list($ns,$name) = $this->splitns($name); + public function __construct($name, $value='', $namespaceURI='') + { + list($ns, $name) = $this->splitns($name); $value = htmlspecialchars($value); - if(!$namespaceURI && $ns){ + 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){ + public function newChild($name, $value='') + { + list($ns, $local) = $this->splitns($name); + if ($ns) { $nsuri = $this->namespaces[$ns]; - if($this->isDefaultNamespace($nsuri)){ + 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); + $node = new EPubDOMElement($name, $value, $nsuri); return $this->appendChild($node); } @@ -50,51 +51,55 @@ public function newChild($name, $value=''){ * @param string $name * @return array (namespace, name) */ - public function splitns($name){ - $list = explode(':',$name,2); - if(count($list) < 2) array_unshift($list,''); + 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); + public function attr($attr, $value=null) + { + list($ns, $attr) = $this->splitns($attr); $nsuri = ''; - if($ns){ + if ($ns) { $nsuri = $this->namespaces[$ns]; - if(!$this->namespaceURI){ - if($this->isDefaultNamespace($nsuri)){ + if (!$this->namespaceURI) { + if ($this->isDefaultNamespace($nsuri)) { $nsuri = ''; } - }elseif($this->namespaceURI == $nsuri){ - $nsuri = ''; + } elseif ($this->namespaceURI == $nsuri) { + $nsuri = ''; } } - if(!is_null($value)){ - if($value === false){ + if (!is_null($value)) { + if ($value === false) { // delete if false was given - if($nsuri){ - $this->removeAttributeNS($nsuri,$attr); - }else{ + if ($nsuri) { + $this->removeAttributeNS($nsuri, $attr); + } else { $this->removeAttribute($attr); } - }else{ + } else { // modify if value was given - if($nsuri){ - $this->setAttributeNS($nsuri,$attr,$value); - }else{ - $this->setAttribute($attr,$value); + if ($nsuri) { + $this->setAttributeNS($nsuri, $attr, $value); + } else { + $this->setAttribute($attr, $value); } } - }else{ + } else { // return value if none was given - if($nsuri){ - return $this->getAttributeNS($nsuri,$attr); - }else{ + if ($nsuri) { + return $this->getAttributeNS($nsuri, $attr); + } else { return $this->getAttribute($attr); } } @@ -103,8 +108,8 @@ public function attr($attr,$value=null){ /** * Remove this node from the DOM */ - public function delete(){ + public function delete() + { $this->parentNode->removeChild($this); } - } diff --git a/lib/EPubDOMXPath.php b/lib/EPubDOMXPath.php index 75d231c..8756f46 100644 --- a/lib/EPubDOMXPath.php +++ b/lib/EPubDOMXPath.php @@ -3,16 +3,19 @@ * PHP EPub Meta library * * @author Andreas Gohr <andi@splitbrain.org> - * @author Sébastien Lucas <sebastien@slucas.fr> + * @author SĂŠbastien Lucas <sebastien@slucas.fr> */ -class EPubDOMXPath extends DOMXPath { - public function __construct(DOMDocument $doc){ +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); + if (is_a($doc->documentElement, 'EPubDOMElement')) + { + foreach ($doc->documentElement->namespaces as $ns => $url) { + $this->registerNamespace($ns, $url); } } } From 9e53f1e6b1a6d0c506fd5c1f2d27c24f7a31521d Mon Sep 17 00:00:00 2001 From: Markus Birth <mbirth@gmail.com> Date: Fri, 1 Jul 2016 12:49:07 +0200 Subject: [PATCH 32/63] Fix unit tests. --- .gitignore | 9 +- composer.json | 1 + composer.lock | 1338 +++++++++++++++++++++++++++++++++++++++++++++ index.php | 8 +- phpunit.xml | 13 +- test/epubTest.php | 3 - 6 files changed, 1357 insertions(+), 15 deletions(-) create mode 100644 composer.lock diff --git a/.gitignore b/.gitignore index 685c5f2..b65eed2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ -coverage/* -php-epub-meta.sublime-* -clover.xml +/coverage/* +/vendor/* +/php-epub-meta.sublime-* +/clover.xml +/composer.phar + diff --git a/composer.json b/composer.json index a3d663f..4a69394 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "Skrol29/tbszip": "dev-master" }, "require-dev": { + "phpunit/phpunit": "5.4.*" }, "autoload": { "classmap": ["lib/"] diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..2eb9584 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1338 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "e52124aa5668fc7168b0adc2294c2886", + "content-hash": "48ee4777edf429b9fa1c4c3704c0ef18", + "packages": [ + { + "name": "Skrol29/tbszip", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Skrol29/tbszip", + "reference": "master" + }, + "type": "library", + "autoload": { + "classmap": [ + "./" + ] + }, + "time": "2014-04-10 23:17:34" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "myclabs/deep-copy", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "a8773992b362b58498eed24bf85005f363c34771" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/a8773992b362b58498eed24bf85005f363c34771", + "reference": "a8773992b362b58498eed24bf85005f363c34771", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2015-11-20 12:04:31" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "9270140b940ff02e58ec577c237274e92cd40cdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9270140b940ff02e58ec577c237274e92cd40cdd", + "reference": "9270140b940ff02e58ec577c237274e92cd40cdd", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-06-10 09:48:41" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-06-10 07:14:17" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-06-07 08:13:47" + }, + { + "name": "phpunit/php-code-coverage", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/900370c81280cc0d942ffbc5912d80464eaee7e9", + "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "^1.4.2", + "sebastian/code-unit-reverse-lookup": "~1.0", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0|~2.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.4.0", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2016-06-03 05:03:56" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-06-21 13:08:43" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2016-05-12 18:03:57" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-09-15 10:49:45" + }, + { + "name": "phpunit/phpunit", + "version": "5.4.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2f1fc94b77ea6418bd6a06c64a1dac0645fbce59", + "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "^4.0", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "^1.3 || ^2.0", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/object-enumerator": "~1.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0|~2.0", + "symfony/yaml": "~2.1|~3.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2016-06-16 06:01:15" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.2.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "b13d0d9426ced06958bd32104653526a6c998a52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/b13d0d9426ced06958bd32104653526a6c998a52", + "reference": "b13d0d9426ced06958bd32104653526a6c998a52", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2016-06-12 07:37:26" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2016-02-13 06:45:14" + }, + { + "name": "sebastian/comparator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-07-26 15:48:44" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/environment", + "version": "1.3.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716", + "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-05-17 03:18:57" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-06-17 09:04:28" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/object-enumerator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2016-01-28 13:25:10" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11 19:50:13" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28 20:34:47" + }, + { + "name": "sebastian/version", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-02-04 12:56:52" + }, + { + "name": "symfony/yaml", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/2884c26ce4c1d61aebf423a8b912950fe7c764de", + "reference": "2884c26ce4c1d61aebf423a8b912950fe7c764de", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-06-29 05:41:56" + }, + { + "name": "webmozart/assert", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", + "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2015-08-24 13:29:44" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "skrol29/tbszip": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.0", + "ext-xml": "*", + "ext-zip": "*" + }, + "platform-dev": [] +} diff --git a/index.php b/index.php index 49c41bd..4ee1d6b 100644 --- a/index.php +++ b/index.php @@ -9,12 +9,8 @@ exit; } - require dirname(__FILE__) . '/util.php'; - - // load epub data - require dirname(__FILE__) . '/lib/EPubDOMElement.php'; - require dirname(__FILE__) . '/lib/EPubDOMXPath.php'; - require dirname(__FILE__) . '/lib/EPub.php'; + require_once dirname(__FILE__) . '/vendor/autoload.php'; + require_once dirname(__FILE__) . '/util.php'; if (isset($_REQUEST['book'])) { try { diff --git a/phpunit.xml b/phpunit.xml index 835461a..cac584a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,21 +1,28 @@ -<phpunit> +<?xml version="1.0" encoding="utf-8"?> +<phpunit + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd" + bootstrap="./vendor/autoload.php" + verbose="true"> <filter> - <whitelist processUncoveredFilesFromWhitelist="true"> + <whitelist processUncoveredFilesFromWhitelist="false"> <!-- this is the path of the files included in your clover report --> <directory suffix=".php">./</directory> <directory suffix=".php">./lib/</directory> <exclude> <directory suffix=".php">./test</directory> + <directory suffix=".php">./vendor</directory> <file>tbszip.php</file> </exclude> </whitelist> </filter> <logging> <log type="coverage-clover" target="./clover.xml"/> + <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/> </logging> <testsuites> <testsuite name="php-epub-meta"> - <directory>test</directory> + <directory>./test/</directory> </testsuite> </testsuites> </phpunit> diff --git a/test/epubTest.php b/test/epubTest.php index 62dd7ab..72c760d 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -1,8 +1,5 @@ <?php -require_once(realpath( dirname( __FILE__ ) ) . '/../epub.php'); - - class EPubTest extends PHPUnit_Framework_TestCase { protected $epub; From 51c504f8def4ef86382c08c10810c955f3684f80 Mon Sep 17 00:00:00 2001 From: Markus Birth <github.com@birth-online.de> Date: Sat, 2 Jul 2016 01:10:12 +0200 Subject: [PATCH 33/63] Fixed requirements, now that seblucas/tbszip is on Packagist. --- composer.json | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index 4a69394..974d11b 100644 --- a/composer.json +++ b/composer.json @@ -22,29 +22,12 @@ "php": ">=5.3.0", "ext-xml": "*", "ext-zip": "*", - "Skrol29/tbszip": "dev-master" + "seblucas/tbszip": "~2.16.0" }, "require-dev": { "phpunit/phpunit": "5.4.*" }, "autoload": { "classmap": ["lib/"] - }, - "repositories": [ - { - "type": "package", - "package": { - "name": "Skrol29/tbszip", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Skrol29/tbszip", - "reference": "master" - }, - "autoload": { - "classmap": ["./"] - } - } - } - ] + } } From 195584eff1e6a9073d2c45ba6b2d741250a022ec Mon Sep 17 00:00:00 2001 From: Markus Birth <github.com@birth-online.de> Date: Sat, 2 Jul 2016 01:11:40 +0200 Subject: [PATCH 34/63] Removed part about external tbszip repo. --- README.md | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/README.md b/README.md index 32972a9..1956ee3 100644 --- a/README.md +++ b/README.md @@ -40,36 +40,5 @@ add these lines to your project's `composer.json`: ``` "require": { "seblucas/php-epub-meta": "dev-master", - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/mbirth/php-epub-meta" - } - ] -``` - -Since this package requires TbsZip, you might have to add the following lines, too: - -``` - "require": { - "Skrol29/tbszip": "dev-master", - }, - "repositories": [ - { - "type": "package", - "package": { - "name": "Skrol29/tbszip", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Skrol29/tbszip", - "reference": "master" - }, - "autoload": { - "classmap": ["./"] - } - } - } - ] + } ``` From e5f62ccac1d7ef97eca7020819d0d68399e99134 Mon Sep 17 00:00:00 2001 From: Markus Birth <mbirth@gmail.com> Date: Sun, 3 Jul 2016 20:34:27 +0200 Subject: [PATCH 35/63] Updated composer.lock. --- composer.lock | 57 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/composer.lock b/composer.lock index 2eb9584..01d93f4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,24 +4,61 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "e52124aa5668fc7168b0adc2294c2886", - "content-hash": "48ee4777edf429b9fa1c4c3704c0ef18", + "hash": "c41f6722f952762d0c100cad16efcdac", + "content-hash": "004535c7a00e78e2eab34fa8b0c5265f", "packages": [ { - "name": "Skrol29/tbszip", - "version": "dev-master", + "name": "seblucas/tbszip", + "version": "2.16.0", "source": { "type": "git", - "url": "https://github.com/Skrol29/tbszip", - "reference": "master" + "url": "https://github.com/mbirth/tbszip.git", + "reference": "3422510ee29b267b779eba550ca543f270a3f4c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mbirth/tbszip/zipball/3422510ee29b267b779eba550ca543f270a3f4c6", + "reference": "3422510ee29b267b779eba550ca543f270a3f4c6", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "5.4.*" }, "type": "library", "autoload": { "classmap": [ - "./" + "tbszip.php" ] }, - "time": "2014-04-10 23:17:34" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1+" + ], + "authors": [ + { + "name": "Skrol29", + "homepage": "http://www.tinybutstrong.com/", + "role": "Developer" + }, + { + "name": "SĂŠbastien Lucas", + "email": "sebastien@slucas.fr", + "homepage": "http://www.slucas.fr/", + "role": "Developer" + } + ], + "description": "Work with zip archives without making temporary files or needing binaries", + "homepage": "http://www.tinybutstrong.com/tools.php", + "keywords": [ + "archive", + "compression", + "zip" + ], + "time": "2016-07-01 22:21:40" } ], "packages-dev": [ @@ -1324,9 +1361,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "skrol29/tbszip": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { From 935626ffa487758e0b5b97d83851d656e9922730 Mon Sep 17 00:00:00 2001 From: Markus Birth <mbirth@gmail.com> Date: Sun, 3 Jul 2016 21:17:36 +0200 Subject: [PATCH 36/63] Fix Travis config (added composer install). --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7af60af..ef90e6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,9 @@ php: - 5.4 - 5.3 - hhvm +before_script: + - composer selfupdate + - composer install script: - phpunit after_success: From 1760c264914ec452ea582bacb541f7195c699349 Mon Sep 17 00:00:00 2001 From: Markus Birth <mbirth@gmail.com> Date: Sun, 3 Jul 2016 21:40:04 +0200 Subject: [PATCH 37/63] Updated Travis config to latest PHP versions. Also use older PHPUnit 4.8 for backwards compatibility. (hopefully) --- .travis.yml | 5 +- composer.json | 2 +- composer.lock | 283 ++++++++------------------------------------------ 3 files changed, 49 insertions(+), 241 deletions(-) diff --git a/.travis.yml b/.travis.yml index ef90e6d..4e33e71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: php php: + - 7.0 + - 5.6 - 5.5 - - 5.4 - - 5.3 - hhvm before_script: - composer selfupdate @@ -15,3 +15,4 @@ after_success: matrix: allow_failures: - php: hhvm + - php: 5.5 diff --git a/composer.json b/composer.json index 974d11b..5357a9c 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "seblucas/tbszip": "~2.16.0" }, "require-dev": { - "phpunit/phpunit": "5.4.*" + "phpunit/phpunit": "4.*" }, "autoload": { "classmap": ["lib/"] diff --git a/composer.lock b/composer.lock index 01d93f4..516ad78 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "c41f6722f952762d0c100cad16efcdac", - "content-hash": "004535c7a00e78e2eab34fa8b0c5265f", + "hash": "b435dd27ef7a0329dff76247c231f9e7", + "content-hash": "cec5f5f6dfcc0b18b9460205b5352896", "packages": [ { "name": "seblucas/tbszip", - "version": "2.16.0", + "version": "2.16.1", "source": { "type": "git", - "url": "https://github.com/mbirth/tbszip.git", - "reference": "3422510ee29b267b779eba550ca543f270a3f4c6" + "url": "https://github.com/seblucas/tbszip.git", + "reference": "2c50bf309bb4431a24e206f164fdb4a2e6b10b7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mbirth/tbszip/zipball/3422510ee29b267b779eba550ca543f270a3f4c6", - "reference": "3422510ee29b267b779eba550ca543f270a3f4c6", + "url": "https://api.github.com/repos/seblucas/tbszip/zipball/2c50bf309bb4431a24e206f164fdb4a2e6b10b7f", + "reference": "2c50bf309bb4431a24e206f164fdb4a2e6b10b7f", "shasum": "" }, "require": { @@ -58,7 +58,7 @@ "compression", "zip" ], - "time": "2016-07-01 22:21:40" + "time": "2016-07-03 12:26:16" } ], "packages-dev": [ @@ -116,48 +116,6 @@ ], "time": "2015-06-14 21:17:01" }, - { - "name": "myclabs/deep-copy", - "version": "1.5.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "a8773992b362b58498eed24bf85005f363c34771" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/a8773992b362b58498eed24bf85005f363c34771", - "reference": "a8773992b362b58498eed24bf85005f363c34771", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2015-11-20 12:04:31" - }, { "name": "phpdocumentor/reflection-common", "version": "1.0", @@ -368,40 +326,39 @@ }, { "name": "phpunit/php-code-coverage", - "version": "4.0.0", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9" + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/900370c81280cc0d942ffbc5912d80464eaee7e9", - "reference": "900370c81280cc0d942ffbc5912d80464eaee7e9", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "php": ">=5.3.3", "phpunit/php-file-iterator": "~1.3", "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "^1.4.2", - "sebastian/code-unit-reverse-lookup": "~1.0", + "phpunit/php-token-stream": "~1.3", "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0|~2.0" + "sebastian/version": "~1.0" }, "require-dev": { "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "^5.4" + "phpunit/phpunit": "~4" }, "suggest": { "ext-dom": "*", - "ext-xdebug": ">=2.4.0", + "ext-xdebug": ">=2.2.1", "ext-xmlwriter": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { @@ -427,7 +384,7 @@ "testing", "xunit" ], - "time": "2016-06-03 05:03:56" + "time": "2015-10-06 15:47:00" }, { "name": "phpunit/php-file-iterator", @@ -612,16 +569,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.4.6", + "version": "4.8.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59" + "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2f1fc94b77ea6418bd6a06c64a1dac0645fbce59", - "reference": "2f1fc94b77ea6418bd6a06c64a1dac0645fbce59", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74", + "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74", "shasum": "" }, "require": { @@ -630,27 +587,21 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", + "php": ">=5.3.3", "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "^4.0", + "phpunit/php-code-coverage": "~2.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", + "phpunit/phpunit-mock-objects": "~2.3", "sebastian/comparator": "~1.1", "sebastian/diff": "~1.2", - "sebastian/environment": "^1.3 || ^2.0", + "sebastian/environment": "~1.3", "sebastian/exporter": "~1.2", "sebastian/global-state": "~1.0", - "sebastian/object-enumerator": "~1.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0|~2.0", + "sebastian/version": "~1.0", "symfony/yaml": "~2.1|~3.0" }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" - }, "suggest": { "phpunit/php-invoker": "~1.1" }, @@ -660,7 +611,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.4.x-dev" + "dev-master": "4.8.x-dev" } }, "autoload": { @@ -686,33 +637,30 @@ "testing", "xunit" ], - "time": "2016-06-16 06:01:15" + "time": "2016-05-17 03:09:28" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.2.3", + "version": "2.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "b13d0d9426ced06958bd32104653526a6c998a52" + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/b13d0d9426ced06958bd32104653526a6c998a52", - "reference": "b13d0d9426ced06958bd32104653526a6c998a52", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" }, "require-dev": { - "phpunit/phpunit": "^5.4" + "phpunit/phpunit": "~4.4" }, "suggest": { "ext-soap": "*" @@ -720,7 +668,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "2.3.x-dev" } }, "autoload": { @@ -745,52 +693,7 @@ "mock", "xunit" ], - "time": "2016-06-12 07:37:26" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2016-02-13 06:45:14" + "time": "2015-10-02 06:51:40" }, { "name": "sebastian/comparator", @@ -1076,52 +979,6 @@ ], "time": "2015-10-12 03:26:01" }, - { - "name": "sebastian/object-enumerator", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "phpunit/phpunit": "~5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2016-01-28 13:25:10" - }, { "name": "sebastian/recursion-context", "version": "1.0.2", @@ -1175,71 +1032,21 @@ "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "time": "2015-11-11 19:50:13" }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28 20:34:47" - }, { "name": "sebastian/version", - "version": "2.0.0", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", "shasum": "" }, - "require": { - "php": ">=5.6" - }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -1258,7 +1065,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-02-04 12:56:52" + "time": "2015-06-21 13:59:46" }, { "name": "symfony/yaml", From d8b3a2cf93c60b14b6c92a974c243dbaa748e572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= <sebastien@slucas.fr> Date: Sun, 3 Jul 2016 22:16:06 +0200 Subject: [PATCH 38/63] =?UTF-8?q?=C3=89tiquette=201.0.0=20ajout=C3=A9e=20?= =?UTF-8?q?=C3=A0=20la=20r=C3=A9vision=20c5bdb12b19a2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hgtags | 1 + 1 file changed, 1 insertion(+) create mode 100644 .hgtags diff --git a/.hgtags b/.hgtags new file mode 100644 index 0000000..6d48e52 --- /dev/null +++ b/.hgtags @@ -0,0 +1 @@ +c5bdb12b19a22820b16b9f252b12e756f1ac8505 1.0.0 From 39620b9ac2b2abe764c705b2a29c8f243190584a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lucas?= <sebastien@slucas.fr> Date: Mon, 4 Jul 2016 21:44:21 +0200 Subject: [PATCH 39/63] Restore the licence file to its original state. --- LICENSE | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 2fcf6f9..1b10fcb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,5 @@ +MIT License + Copyright (c) 2012 Andreas Gohr <andi@splitbrain.org> Permission is hereby granted, free of charge, to any person obtaining a copy of @@ -17,6 +19,3 @@ 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. - -All code created or modified by Sébastien Lucas <sebastien@slucas.fr> is licensed -under GPL 2 (http://www.gnu.org/licenses/gpl.html). \ No newline at end of file From 55725b3832b47f507f74ec6a789e13760e2a0e99 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 6 Jul 2016 21:37:10 +0200 Subject: [PATCH 40/63] adjusted package name --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5357a9c..92dc788 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "seblucas/php-epub-meta", + "name": "splitbrain/php-epub-meta", "type": "library", "description": "Reading and writing metadata included in the EPub ebook format", "keywords": ["epub", "metadata", "ebook"], From 14460c3799493175255a8ef8e6b96126eb7b2516 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 6 Jul 2016 21:40:52 +0200 Subject: [PATCH 41/63] fixed some undefined indexes in the manager --- index.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.php b/index.php index 4ee1d6b..ea199e1 100644 --- a/index.php +++ b/index.php @@ -31,7 +31,7 @@ } // save epub data - if ($_REQUEST['save'] && isset($epub)) { + if (!empty($_REQUEST['save']) && isset($epub)) { $epub->Title($_POST['title']); $epub->Description($_POST['description']); $epub->Language($_POST['language']); @@ -112,7 +112,7 @@ <link rel="stylesheet" type="text/css" href="assets/css/style.css" /> <script type="text/javascript"> - <?php if($error) echo "alert('" . htmlspecialchars($error) . "');";?> + <?php if(isset($error)) echo "alert('" . htmlspecialchars($error) . "');";?> </script> </head> <body> @@ -124,14 +124,14 @@ foreach ($list as $book) { $base = basename($book, '.epub'); $name = book_output($base); - echo '<li ' . ($base == $_REQUEST['book'] ? 'class="active"' : '' ) . '>'; + echo '<li ' . (isset($_REQUEST['book']) && $base == $_REQUEST['book'] ? 'class="active"' : '' ) . '>'; echo '<a href="?book=' . htmlspecialchars($base) . '">' . $name . '</a>'; echo '</li>'; } ?> </ul> - <?php if($epub): ?> + <?php if(isset($epub)): ?> <form action="" method="post" id="bookpanel" enctype="multipart/form-data"> <input type="hidden" name="book" value="<?php echo htmlspecialchars($_REQUEST['book'])?>" /> From 0ebeafefb73a73260da60699857a1631ba56941f Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 6 Jul 2016 22:29:35 +0200 Subject: [PATCH 42/63] some first cleanup of the type hinting --- lib/EPub.php | 31 ++++++++++++++++++++++--------- lib/EPubDOMNodeList.php | 13 +++++++++++++ lib/EPubDOMXPath.php | 12 ++++++++++++ 3 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 lib/EPubDOMNodeList.php diff --git a/lib/EPub.php b/lib/EPub.php index f0e5979..66c2576 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -10,12 +10,16 @@ class EPub { + /** @var EPubDOMXPath */ public $xml; //FIXME: change to protected, later public $toc; + /** @var EPubDOMXPath */ protected $xpath; + /** @var EPubDOMXPath */ protected $toc_xpath; protected $file; protected $meta; + /** @var clsTbsZip */ protected $zip; protected $coverpath=''; protected $namespaces; @@ -71,6 +75,7 @@ public function __construct($file, $zipClass = 'clsTbsZip') public function initSpineComponent() { + /** @var EPubDOMElement $spine */ $spine = $this->xpath->query('//opf:spine')->item(0); $tocid = $spine->getAttribute('toc'); $tochref = $this->xpath->query('//opf:manifest/opf:item[@id="' . $tocid . '"]')->item(0)->attr('href'); @@ -126,7 +131,7 @@ public function cleanITunesCrap() public function save() { $this->download(); - $this->zip->close(); + $this->zip->Close(); } /** @@ -153,6 +158,7 @@ public function components() $spine = array(); $nodes = $this->xpath->query('//opf:spine/opf:itemref'); foreach ($nodes as $node) { + /** @var EPubDOMElement $node */ $idref = $node->getAttribute('idref'); $spine[] = $this->encodeComponentName($this->xpath->query('//opf:manifest/opf:item[@id="' . $idref . '"]')->item(0)->getAttribute('href')); } @@ -274,6 +280,8 @@ public function contents() * ) * * @params array $authors + * @param bool $authors + * @return array */ public function Authors($authors=false) { @@ -292,6 +300,7 @@ public function Authors($authors=false) // delete existing nodes $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); foreach ($nodes as $node) { + /** @var EPubDOMElement $node */ $node->delete(); } @@ -336,7 +345,8 @@ public function Authors($authors=false) /** * Set or get the book title * - * @param string $title + * @param bool|string $title + * @return string */ public function Title($title=false) { @@ -386,7 +396,7 @@ public function Description($description=false) /** * Set or get the book's Unique Identifier * - * @param string Unique identifier + * @param string $uuid Unique identifier */ public function Uuid($uuid = false) { @@ -651,7 +661,7 @@ public function getCoverItem() return $nodes->item(0); } - public function Combine($a, $b) + public function combine($a, $b) { $isAbsolute = false; if ($a[0] == '/') { @@ -749,11 +759,12 @@ public function Cover2($path=false, $mime=false) * * 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 + * @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 + * @return string */ protected function getset($item, $value=false, $att=false, $aval=false, $datt=false) { @@ -783,6 +794,7 @@ protected function getset($item, $value=false, $att=false, $aval=false, $datt=fa // 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) { + /** @var EPubDOMElement $n */ $n->delete(); } // readd them @@ -790,6 +802,7 @@ protected function getset($item, $value=false, $att=false, $aval=false, $datt=fa $parent = $this->xpath->query('//opf:metadata')->item(0); $node = $parent->newChild($item); + /** @var EPubDOMElement $node */ if ($att) { $node->attr($att, $aval); } diff --git a/lib/EPubDOMNodeList.php b/lib/EPubDOMNodeList.php new file mode 100644 index 0000000..2df203f --- /dev/null +++ b/lib/EPubDOMNodeList.php @@ -0,0 +1,13 @@ +<?php + +class EPubDOMNodeList extends DOMNodeList { + /** + * @link http://php.net/manual/en/domnodelist.item.php + * @param int $index + * @return EPubDOMElement + */ + public function item($index) { + return parent::item($index); + } + +} \ No newline at end of file diff --git a/lib/EPubDOMXPath.php b/lib/EPubDOMXPath.php index 8756f46..b27f0e6 100644 --- a/lib/EPubDOMXPath.php +++ b/lib/EPubDOMXPath.php @@ -19,4 +19,16 @@ public function __construct(DOMDocument $doc) } } } + + /** + * Evaluates the given XPath expression + * @link http://php.net/manual/en/domxpath.query.php + * @param string $expression + * @param DOMNode $contextnode + * @return EpubDOMNodeList + */ + public function query($expression, $contextnode = null) { + return parent::query($expression, $contextnode); + } + } From f060c687e128bd65637cede8119445e079ad8818 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 6 Jul 2016 22:32:28 +0200 Subject: [PATCH 43/63] fixed undefined variable --- lib/EPub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/EPub.php b/lib/EPub.php index 66c2576..7677388 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -669,7 +669,7 @@ public function combine($a, $b) } if ($b[0] == '/') { - throw new InvalidArgumentException('Second path part must not start with ' . $m_Separator); + throw new InvalidArgumentException('Second path part must not start with /'); } $splittedA = preg_split('#/#', $a); From a6f6090a51f8b4a456b1e5d71fa22a89ae16f321 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 6 Jul 2016 22:59:05 +0200 Subject: [PATCH 44/63] use null as a default for getters, update type hints --- lib/EPub.php | 173 +++++++++++++++++++++++++++------------------------ 1 file changed, 92 insertions(+), 81 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index 7677388..e2c152f 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -266,6 +266,8 @@ public function contents() return $contents; } + #region Book Attribute Getter/Setters + /** * Get or set the book author(s) * @@ -279,17 +281,17 @@ public function contents() * 'Simpson, Jacqueline' => 'Jacqueline Simpson', * ) * - * @params array $authors - * @param bool $authors + * When a string is given, it assumed to be a comma separted list of Author names + * + * @param array|string|null $authors * @return array */ - public function Authors($authors=false) - { + public function Authors($authors = null) { // set new data - if ($authors !== false) { + if($authors !== null) { // Author where given as a comma separated list - if (is_string($authors)) { - if ($authors == '') { + if(is_string($authors)) { + if($authors == '') { $authors = array(); } else { $authors = explode(',', $authors); @@ -299,15 +301,15 @@ public function Authors($authors=false) // delete existing nodes $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); - foreach ($nodes as $node) { + foreach($nodes as $node) { /** @var EPubDOMElement $node */ $node->delete(); } // add new nodes $parent = $this->xpath->query('//opf:metadata')->item(0); - foreach ($authors as $as => $name) { - if (is_int($as)) { + foreach($authors as $as => $name) { + if(is_int($as)) { $as = $name; //numeric array given } $node = $parent->newChild('dc:creator', $name); @@ -322,19 +324,19 @@ public function Authors($authors=false) $rolefix = false; $authors = array(); $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); - if ($nodes->length == 0) { + 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) { + foreach($nodes as $node) { $name = $node->nodeValue; - $as = $node->attr('opf:file-as'); - if (!$as) { + $as = $node->attr('opf:file-as'); + if(!$as) { $as = $name; $node->attr('opf:file-as', $as); } - if ($rolefix) { + if($rolefix) { $node->attr('opf:role', 'aut'); } $authors[$as] = $name; @@ -345,65 +347,65 @@ public function Authors($authors=false) /** * Set or get the book title * - * @param bool|string $title + * @param string|null $title * @return string */ - public function Title($title=false) - { + public function Title($title = null) { return $this->getset('dc:title', $title); } /** * Set or get the book's language * - * @param string $lang + * @param string|null $lang + * @return string */ - public function Language($lang=false) - { + public function Language($lang = null) { return $this->getset('dc:language', $lang); } /** * Set or get the book' publisher info * - * @param string $publisher + * @param string|null $publisher + * @return string */ - public function Publisher($publisher=false) - { + public function Publisher($publisher = null) { return $this->getset('dc:publisher', $publisher); } /** * Set or get the book's copyright info * - * @param string $rights + * @param string|null $rights + * @return string */ - public function Copyright($rights=false) - { + public function Copyright($rights = null) { return $this->getset('dc:rights', $rights); } /** * Set or get the book's description * - * @param string $description + * @param string|null $description + * @return string */ - public function Description($description=false) - { + public function Description($description = null) { return $this->getset('dc:description', $description); } /** * Set or get the book's Unique Identifier * - * @param string $uuid Unique identifier + * @param string|null $uuid Unique identifier + * @return string + * @throws Exception + * @todo auto add unique identifer if needed */ - public function Uuid($uuid = false) - { + public function Uuid($uuid = null) { $nodes = $this->xpath->query('/opf:package'); - if ($nodes->length !== 1) { - $error = sprintf('Cannot find ebook identifier'); - throw new Exception($error); + if($nodes->length !== 1) { + throw new Exception('Cannot find ebook identifier'); } $identifier = $nodes->item(0)->attr('unique-identifier'); @@ -415,10 +417,11 @@ public function Uuid($uuid = false) /** * Set or get the book's creation date * - * @param string Date eg: 2012-05-19T12:54:25Z + * @param string|null $date Date eg: 2012-05-19T12:54:25Z + * @todo use DateTime class instead of string + * @return string */ - public function CreationDate($date = false) - { + public function CreationDate($date = null) { $res = $this->getset('dc:date', $date, 'opf:event', 'creation'); return $res; @@ -427,10 +430,11 @@ public function CreationDate($date = false) /** * Set or get the book's modification date * - * @param string Date eg: 2012-05-19T12:54:25Z + * @param string|null $date Date eg: 2012-05-19T12:54:25Z + * @todo use DateTime class instead of string + * @return string */ - public function ModificationDate($date = false) - { + public function ModificationDate($date = null) { $res = $this->getset('dc:date', $date, 'opf:event', 'modification'); return $res; @@ -439,10 +443,10 @@ public function ModificationDate($date = false) /** * Set or get the book's URI * - * @param string URI + * @param string|null $uri URI + * @return string */ - public function Uri($uri = false) - { + public function Uri($uri = null) { $res = $this->getset('dc:identifier', $uri, 'opf:scheme', 'URI'); return $res; @@ -451,60 +455,60 @@ public function Uri($uri = false) /** * Set or get the book's ISBN number * - * @param string $isbn + * @param string|null $isbn + * @return string */ - public function ISBN($isbn=false) - { + public function ISBN($isbn = null) { return $this->getset('dc:identifier', $isbn, 'opf:scheme', 'ISBN'); } /** * Set or get the Google Books ID * - * @param string $google + * @param string|null $google + * @return string */ - public function Google($google=false) - { + public function Google($google = null) { return $this->getset('dc:identifier', $google, 'opf:scheme', 'GOOGLE'); } /** * Set or get the Amazon ID of the book * - * @param string $amazon + * @param string|null $amazon + * @return string */ - public function Amazon($amazon=false) - { + public function Amazon($amazon = null) { return $this->getset('dc:identifier', $amazon, 'opf:scheme', 'AMAZON'); } /** * Set or get the Calibre UUID of the book * - * @param string $uuid + * @param null|string $uuid + * @return string */ - public function Calibre($uuid=false) - { + public function Calibre($uuid = null) { return $this->getset('dc:identifier', $uuid, 'opf:scheme', 'calibre'); } /** * Set or get the Serie of the book * - * @param string $serie + * @param string|null $serie + * @return string */ - public function Serie($serie=false) - { + public function Serie($serie = null) { return $this->getset('opf:meta', $serie, 'name', 'calibre:series', 'content'); } /** * Set or get the Serie Index of the book * - * @param string $serieIndex + * @param string|null $serieIndex + * @return string */ - public function SerieIndex($serieIndex=false) - { + public function SerieIndex($serieIndex = null) { return $this->getset('opf:meta', $serieIndex, 'name', 'calibre:series_index', 'content'); } @@ -514,14 +518,14 @@ public function SerieIndex($serieIndex=false) * Subject should be given as array, but a comma separated string will also * be accepted. * - * @param array $subjects + * @param array|string|null $subjects + * @return string[] */ - public function Subjects($subjects=false) - { + public function Subjects($subjects = null) { // setter - if ($subjects !== false) { - if (is_string($subjects)) { - if ($subjects === '') { + if($subjects !== null) { + if(is_string($subjects)) { + if($subjects === '') { $subjects = array(); } else { $subjects = explode(',', $subjects); @@ -531,14 +535,15 @@ public function Subjects($subjects=false) // delete previous $nodes = $this->xpath->query('//opf:metadata/dc:subject'); - foreach ($nodes as $node) { + foreach($nodes as $node) { + /** @var EPubDOMElement $node */ $node->delete(); } // add new ones $parent = $this->xpath->query('//opf:metadata')->item(0); - foreach ($subjects as $subj) { + foreach($subjects as $subj) { $node = $this->xml->createElement('dc:subject', htmlspecialchars($subj)); - $node = $parent->appendChild($node); + $parent->appendChild($node); } $this->reparse(); @@ -547,12 +552,14 @@ public function Subjects($subjects=false) //getter $subjects = array(); $nodes = $this->xpath->query('//opf:metadata/dc:subject'); - foreach ($nodes as $node) { - $subjects[] = $node->nodeValue; + foreach($nodes as $node) { + $subjects[] = $node->nodeValue; } return $subjects; } + #endregion + /** * Read the cover data * @@ -760,22 +767,26 @@ public function Cover2($path=false, $mime=false) * 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 + * @param string|null $value New value to set, null to get, passing an empty string deletes the node + * @param string|null $att Attribute name the node needs to have for a match + * @param string|null $aval Attribute value the node needs to have for a match + * @param string|null $datt Destination attribute to set instead of the node value * @return string */ - protected function getset($item, $value=false, $att=false, $aval=false, $datt=false) + protected function getset($item, $value=null, $att=null, $aval=null, $datt=null) { // construct xpath $xpath = '//opf:metadata/' . $item; if ($att) { - $xpath .= '[@' . $att . '="' . $aval . '"]'; + if($aval) { + $xpath .= '[@' . $att . '="' . $aval . '"]'; + } else { + $xpath .= '[@' . $att . ']'; + } } // set value - if ($value !== false) { + if ($value !== null) { $value = htmlspecialchars($value); $nodes = $this->xpath->query($xpath); if ($nodes->length == 1 ) { From 4fa07c8f721264e5d1dfca8ad928831d9446c4ad Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 6 Jul 2016 23:00:37 +0200 Subject: [PATCH 45/63] rename serie to correct series --- lib/EPub.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index e2c152f..c0e6562 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -493,23 +493,23 @@ public function Calibre($uuid = null) { } /** - * Set or get the Serie of the book + * Set or get the Series of the book * - * @param string|null $serie + * @param string|null $series * @return string */ - public function Serie($serie = null) { - return $this->getset('opf:meta', $serie, 'name', 'calibre:series', 'content'); + public function Series($series = null) { + return $this->getset('opf:meta', $series, 'name', 'calibre:series', 'content'); } /** - * Set or get the Serie Index of the book + * Set or get the Series Index of the book * - * @param string|null $serieIndex + * @param string|null $seriesIndex * @return string */ - public function SerieIndex($serieIndex = null) { - return $this->getset('opf:meta', $serieIndex, 'name', 'calibre:series_index', 'content'); + public function SeriesIndex($seriesIndex = null) { + return $this->getset('opf:meta', $seriesIndex, 'name', 'calibre:series_index', 'content'); } /** From f2d67db8dc2ab88541cb4ccdbfeb849d4fb71df9 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 6 Jul 2016 23:22:53 +0200 Subject: [PATCH 46/63] more type hints --- lib/EPub.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/EPub.php b/lib/EPub.php index c0e6562..5266725 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -587,11 +587,13 @@ public function Cover($path=false, $mime=false) // remove current pointer $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); foreach ($nodes as $node) { + /** @var EPubDOMElement $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) { + /** @var EPubDOMElement $node */ $node->delete(); } From ede178ea67e09450396066b3daacfdd74909c66b Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 9 Jul 2016 21:27:38 +0200 Subject: [PATCH 47/63] fixed order in tests, expectations should be first --- test/epubTest.php | 86 +++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/test/epubTest.php b/test/epubTest.php index 72c760d..a74ce81 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -1,7 +1,7 @@ <?php class EPubTest extends PHPUnit_Framework_TestCase { - + /** @var EPub */ protected $epub; protected function setUp(){ @@ -26,133 +26,133 @@ public static function tearDownAfterClass() public function testAuthors(){ // read curent value $this->assertEquals( - $this->epub->Authors(), - array('Shakespeare, William' => 'William Shakespeare') + array('Shakespeare, William' => 'William Shakespeare'), + $this->epub->Authors() ); // remove value with string $this->assertEquals( - $this->epub->Authors(''), - array() + array(), + $this->epub->Authors('') ); // set single value by String $this->assertEquals( - $this->epub->Authors('John Doe'), - array('John Doe' => 'John Doe') + array('John Doe' => 'John Doe'), + $this->epub->Authors('John Doe') ); // set single value by indexed array $this->assertEquals( - $this->epub->Authors(array('John Doe')), - array('John Doe' => 'John Doe') + array('John Doe' => 'John Doe'), + $this->epub->Authors(array('John Doe')) ); // remove value with array $this->assertEquals( - $this->epub->Authors(array()), - array() + array(), + $this->epub->Authors(array()) ); // set single value by associative array $this->assertEquals( - $this->epub->Authors(array('Doe, John' => 'John Doe')), - array('Doe, John' => 'John Doe') + array('Doe, John' => 'John Doe'), + $this->epub->Authors(array('Doe, John' => 'John Doe')) ); // set multi value by string $this->assertEquals( - $this->epub->Authors('John Doe, Jane Smith'), - array('John Doe' => 'John Doe', 'Jane Smith' => 'Jane Smith') + array('John Doe' => 'John Doe', 'Jane Smith' => 'Jane Smith'), + $this->epub->Authors('John Doe, Jane Smith') ); // set multi value by indexed array $this->assertEquals( - $this->epub->Authors(array('John Doe', 'Jane Smith')), - array('John Doe' => 'John Doe', 'Jane Smith' => 'Jane Smith') + array('John Doe' => 'John Doe', 'Jane Smith' => 'Jane Smith'), + $this->epub->Authors(array('John Doe', 'Jane Smith')) ); // set multi value by associative array $this->assertEquals( - $this->epub->Authors(array('Doe, John' => 'John Doe', 'Smith, Jane' => 'Jane Smith')), - array('Doe, John' => 'John Doe', 'Smith, Jane' => 'Jane Smith') + array('Doe, John' => 'John Doe', 'Smith, Jane' => 'Jane Smith'), + $this->epub->Authors(array('Doe, John' => 'John Doe', 'Smith, Jane' => 'Jane Smith')) ); // check escaping $this->assertEquals( - $this->epub->Authors(array('Doe, John ' => 'John Doe ')), - array('Doe, John ' => 'John Doe ') + array('Doe, John ' => 'John Doe '), + $this->epub->Authors(array('Doe, John ' => 'John Doe ')) ); } public function testTitle(){ // get current value $this->assertEquals( - $this->epub->Title(), - 'Romeo and Juliet' + 'Romeo and Juliet', + $this->epub->Title() ); // delete current value $this->assertEquals( - $this->epub->Title(''), - '' + '', + $this->epub->Title('') ); // get current value $this->assertEquals( - $this->epub->Title(), - '' + '', + $this->epub->Title() ); // set new value $this->assertEquals( - $this->epub->Title('Foo Bar'), - 'Foo Bar' + 'Foo Bar', + $this->epub->Title('Foo Bar') ); // check escaping $this->assertEquals( - $this->epub->Title('Foo Bar'), - 'Foo Bar' + 'Foo Bar', + $this->epub->Title('Foo Bar') ); } public function testSubject(){ // get current values $this->assertEquals( - $this->epub->Subjects(), - array('Fiction','Drama','Romance') + array('Fiction','Drama','Romance'), + $this->epub->Subjects() ); // delete current values with String $this->assertEquals( - $this->epub->Subjects(''), - array() + array(), + $this->epub->Subjects('') ); // set new values with String $this->assertEquals( - $this->epub->Subjects('Fiction, Drama, Romance'), - array('Fiction','Drama','Romance') + array('Fiction','Drama','Romance'), + $this->epub->Subjects('Fiction, Drama, Romance') ); // delete current values with Array $this->assertEquals( - $this->epub->Subjects(array()), - array() + array(), + $this->epub->Subjects(array()) ); // set new values with array $this->assertEquals( - $this->epub->Subjects(array('Fiction','Drama','Romance')), - array('Fiction','Drama','Romance') + array('Fiction','Drama','Romance'), + $this->epub->Subjects(array('Fiction','Drama','Romance')) ); // check escaping $this->assertEquals( - $this->epub->Subjects(array('Fiction','Drama ','Romance')), - array('Fiction','Drama ','Romance') + array('Fiction','Drama ','Romance'), + $this->epub->Subjects(array('Fiction','Drama ','Romance')) ); } From f1fa98ae5b167e537b707ea6f28d68b5c65115d2 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 9 Jul 2016 22:07:50 +0200 Subject: [PATCH 48/63] introduced a namespace --- composer.json | 65 +++++++++++++++++++++++------------------ composer.lock | 4 +-- lib/EPub.php | 42 +++++++++++++------------- lib/EPubDOMElement.php | 8 ++++- lib/EPubDOMNodeList.php | 4 ++- lib/EPubDOMXPath.php | 10 ++++--- test/epubTest.php | 6 +++- 7 files changed, 81 insertions(+), 58 deletions(-) diff --git a/composer.json b/composer.json index 92dc788..9d33bfe 100644 --- a/composer.json +++ b/composer.json @@ -1,33 +1,40 @@ { - "name": "splitbrain/php-epub-meta", - "type": "library", - "description": "Reading and writing metadata included in the EPub ebook format", - "keywords": ["epub", "metadata", "ebook"], - "homepage": "https://github.com/seblucas/php-epub-meta", - "authors": [ - { - "name": "Andreas Gohr", - "email": "andi@splitbrain.org", - "homepage": "https://www.splitbrain.org/", - "role": "Developer" - }, - { - "name": "SĂŠbastien Lucas", - "email": "sebastien@slucas.fr", - "homepage": "http://www.slucas.fr/", - "role": "Developer" - } - ], - "require": { - "php": ">=5.3.0", - "ext-xml": "*", - "ext-zip": "*", - "seblucas/tbszip": "~2.16.0" + "name": "splitbrain/php-epub-meta", + "type": "library", + "description": "Reading and writing metadata included in the EPub ebook format", + "keywords": [ + "epub", + "metadata", + "ebook" + ], + "homepage": "https://github.com/splitbrain/php-epub-meta", + "license": "MIT", + "authors": [ + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org", + "homepage": "https://www.splitbrain.org/", + "role": "Developer" }, - "require-dev": { - "phpunit/phpunit": "4.*" - }, - "autoload": { - "classmap": ["lib/"] + { + "name": "SĂŠbastien Lucas", + "email": "sebastien@slucas.fr", + "homepage": "http://www.slucas.fr/", + "role": "Developer" + } + ], + "require": { + "php": ">=5.3.0", + "ext-xml": "*", + "ext-zip": "*", + "seblucas/tbszip": "~2.16.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "autoload": { + "psr-4": { + "splitbrain\\epubmeta\\": "lib" } + } } diff --git a/composer.lock b/composer.lock index 516ad78..0532e45 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "b435dd27ef7a0329dff76247c231f9e7", - "content-hash": "cec5f5f6dfcc0b18b9460205b5352896", + "hash": "924b40829f78ea2af3de2fc516d36709", + "content-hash": "d0841a1d14a8e6ea6305431ea76f1267", "packages": [ { "name": "seblucas/tbszip", diff --git a/lib/EPub.php b/lib/EPub.php index 5266725..0f93427 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -6,6 +6,8 @@ * @author SĂŠbastien Lucas <sebastien@slucas.fr> */ +namespace splitbrain\epubmeta; + define('METADATA_FILE', 'META-INF/container.xml'); class EPub @@ -19,7 +21,7 @@ class EPub protected $toc_xpath; protected $file; protected $meta; - /** @var clsTbsZip */ + /** @var \clsTbsZip */ protected $zip; protected $coverpath=''; protected $namespaces; @@ -30,7 +32,7 @@ class EPub * * @param string $file path to epub file to work on * @param string $zipClass class to handle zip - * @throws Exception if metadata could not be loaded + * @throws \Exception if metadata could not be loaded */ public function __construct($file, $zipClass = 'clsTbsZip') { @@ -38,20 +40,20 @@ public function __construct($file, $zipClass = 'clsTbsZip') $this->file = $file; $this->zip = new $zipClass(); if (!$this->zip->Open($this->file)) { - throw new Exception('Failed to read epub 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'); + 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'); + throw new \Exception('Failed to access epub container data'); } - $xml = new DOMDocument(); - $xml->registerNodeClass('DOMElement', 'EPubDOMElement'); + $xml = new \DOMDocument(); + $xml->registerNodeClass('DOMElement', '\\splitbrain\\epubmeta\\EPubDOMElement'); $xml->loadXML($data); $xpath = new EPubDOMXPath($xml); $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]'); @@ -59,15 +61,15 @@ public function __construct($file, $zipClass = 'clsTbsZip') // load metadata if (!$this->zip->FileExists($this->meta)) { - throw new Exception('Unable to find ' . $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'); + throw new \Exception('Failed to access epub metadata'); } - $this->xml = new DOMDocument(); - $this->xml->registerNodeClass('DOMElement', 'EPubDOMElement'); + $this->xml = new \DOMDocument(); + $this->xml->registerNodeClass('DOMElement', '\\splitbrain\\epubmeta\\EPubDOMElement'); $this->xml->loadXML($data); $this->xml->formatOutput = true; $this->xpath = new EPubDOMXPath($this->xml); @@ -82,12 +84,12 @@ public function initSpineComponent() $tocpath = $this->getFullPath($tochref); // read epub toc if (!$this->zip->FileExists($tocpath)) { - throw new Exception('Unable to find ' . $tocpath); + throw new \Exception('Unable to find ' . $tocpath); } $data = $this->zip->FileRead($tocpath); - $this->toc = new DOMDocument(); - $this->toc->registerNodeClass('DOMElement', 'EPubDOMElement'); + $this->toc = new \DOMDocument(); + $this->toc->registerNodeClass('DOMElement', '\\splitbrain\\epubmeta\\EPubDOMElement'); $this->toc->loadXML($data); $this->toc_xpath = new EPubDOMXPath($this->toc); $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI); @@ -173,7 +175,7 @@ public function component($comp) $path = $this->decodeComponentName($comp); $path = $this->getFullPath($path); if (!$this->zip->FileExists($path)) { - throw new Exception('Unable to find ' . $path . ' <' . $comp . '>'); + throw new \Exception('Unable to find ' . $path . ' <' . $comp . '>'); } $data = $this->zip->FileRead($path); @@ -399,13 +401,13 @@ public function Description($description = null) { * * @param string|null $uuid Unique identifier * @return string - * @throws Exception + * @throws \Exception * @todo auto add unique identifer if needed */ public function Uuid($uuid = null) { $nodes = $this->xpath->query('/opf:package'); if($nodes->length !== 1) { - throw new Exception('Cannot find ebook identifier'); + throw new \Exception('Cannot find ebook identifier'); } $identifier = $nodes->item(0)->attr('unique-identifier'); @@ -637,9 +639,9 @@ public function Cover($path=false, $mime=false) $path = dirname('/' . $this->meta) . '/' . $path; // image path is relative to meta file $path = ltrim($path, '/'); - $zip = new ZipArchive(); + $zip = new \ZipArchive(); if (!@$zip->open($this->file)) { - throw new Exception('Failed to read epub file'); + throw new \Exception('Failed to read epub file'); } $data = $zip->getFromName($path); @@ -678,7 +680,7 @@ public function combine($a, $b) } if ($b[0] == '/') { - throw new InvalidArgumentException('Second path part must not start with /'); + throw new \InvalidArgumentException('Second path part must not start with /'); } $splittedA = preg_split('#/#', $a); diff --git a/lib/EPubDOMElement.php b/lib/EPubDOMElement.php index 976ca85..28711c8 100644 --- a/lib/EPubDOMElement.php +++ b/lib/EPubDOMElement.php @@ -6,7 +6,9 @@ * @author SĂŠbastien Lucas <sebastien@slucas.fr> */ -class EPubDOMElement extends DOMElement +namespace splitbrain\epubmeta; + +class EPubDOMElement extends \DOMElement { public $namespaces = array( 'n' => 'urn:oasis:names:tc:opendocument:xmlns:container', @@ -28,9 +30,13 @@ public function __construct($name, $value='', $namespaceURI='') * Create and append a new child * * Works with our epub namespaces and omits default namespaces + * @param string $name + * @param string $value + * @return \DOMNode */ public function newChild($name, $value='') { + $nsuri = ''; list($ns, $local) = $this->splitns($name); if ($ns) { $nsuri = $this->namespaces[$ns]; diff --git a/lib/EPubDOMNodeList.php b/lib/EPubDOMNodeList.php index 2df203f..32502d1 100644 --- a/lib/EPubDOMNodeList.php +++ b/lib/EPubDOMNodeList.php @@ -1,6 +1,8 @@ <?php -class EPubDOMNodeList extends DOMNodeList { +namespace splitbrain\epubmeta; + +class EPubDOMNodeList extends \DOMNodeList { /** * @link http://php.net/manual/en/domnodelist.item.php * @param int $index diff --git a/lib/EPubDOMXPath.php b/lib/EPubDOMXPath.php index b27f0e6..eb763ba 100644 --- a/lib/EPubDOMXPath.php +++ b/lib/EPubDOMXPath.php @@ -6,13 +6,15 @@ * @author SĂŠbastien Lucas <sebastien@slucas.fr> */ -class EPubDOMXPath extends DOMXPath +namespace splitbrain\epubmeta; + +class EPubDOMXPath extends \DOMXPath { - public function __construct(DOMDocument $doc) + public function __construct(\DOMDocument $doc) { parent::__construct($doc); - if (is_a($doc->documentElement, 'EPubDOMElement')) + if (is_a($doc->documentElement, '\\splitbrain\\epubmeta\\EPubDOMElement')) { foreach ($doc->documentElement->namespaces as $ns => $url) { $this->registerNamespace($ns, $url); @@ -24,7 +26,7 @@ public function __construct(DOMDocument $doc) * Evaluates the given XPath expression * @link http://php.net/manual/en/domxpath.query.php * @param string $expression - * @param DOMNode $contextnode + * @param \DOMNode $contextnode * @return EpubDOMNodeList */ public function query($expression, $contextnode = null) { diff --git a/test/epubTest.php b/test/epubTest.php index a74ce81..0613c4f 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -1,6 +1,10 @@ <?php -class EPubTest extends PHPUnit_Framework_TestCase { +namespace splitbrain\epubmeta\test; + +use splitbrain\epubmeta\EPub; + +class EPubTest extends \PHPUnit_Framework_TestCase { /** @var EPub */ protected $epub; From 83b9a7d2c1e48aee9bc00153a3fb16f9d818bd38 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 9 Jul 2016 22:53:22 +0200 Subject: [PATCH 49/63] more cleanup and type hinting --- lib/EPub.php | 238 ++++++++++++++++++++++------------------ lib/EPubDOMDocument.php | 23 ++++ lib/EPubDOMElement.php | 12 +- lib/EPubDOMNodeList.php | 6 +- lib/EPubDOMXPath.php | 12 +- test/Epub.php | 15 +++ test/epubTest.php | 36 +++--- 7 files changed, 202 insertions(+), 140 deletions(-) create mode 100644 lib/EPubDOMDocument.php create mode 100644 test/Epub.php diff --git a/lib/EPub.php b/lib/EPub.php index 0f93427..5eb7dbc 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -12,20 +12,27 @@ class EPub { - /** @var EPubDOMXPath */ - public $xml; //FIXME: change to protected, later - public $toc; - /** @var EPubDOMXPath */ - protected $xpath; - /** @var EPubDOMXPath */ + + /** @var string Location of the meta package within the epub */ + protected $meta; + /** @var EPubDOMDocument Parsed XML of the meta package */ + public $meta_xml; + /** @var EPubDOMXPath XPath access to the meta package */ + protected $meta_xpath; + + /** @var EPubDOMDocument The Table of Contents file */ + public $toc_xml; + /** @var EPubDOMXPath XPath access to the TOC */ protected $toc_xpath; + + /** @var string The path to the epub file */ protected $file; - protected $meta; + /** @var \clsTbsZip */ protected $zip; - protected $coverpath=''; + protected $coverpath = ''; protected $namespaces; - protected $imagetoadd=''; + protected $imagetoadd = ''; /** * Constructor @@ -52,8 +59,7 @@ public function __construct($file, $zipClass = 'clsTbsZip') if ($data == false) { throw new \Exception('Failed to access epub container data'); } - $xml = new \DOMDocument(); - $xml->registerNodeClass('DOMElement', '\\splitbrain\\epubmeta\\EPubDOMElement'); + $xml = new EPubDOMDocument(); $xml->loadXML($data); $xpath = new EPubDOMXPath($xml); $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]'); @@ -68,19 +74,18 @@ public function __construct($file, $zipClass = 'clsTbsZip') if (!$data) { throw new \Exception('Failed to access epub metadata'); } - $this->xml = new \DOMDocument(); - $this->xml->registerNodeClass('DOMElement', '\\splitbrain\\epubmeta\\EPubDOMElement'); - $this->xml->loadXML($data); - $this->xml->formatOutput = true; - $this->xpath = new EPubDOMXPath($this->xml); + $this->meta_xml = new EpubDOMDocument(); + $this->meta_xml->loadXML($data); + $this->meta_xml->formatOutput = true; + $this->meta_xpath = new EPubDOMXPath($this->meta_xml); } public function initSpineComponent() { /** @var EPubDOMElement $spine */ - $spine = $this->xpath->query('//opf:spine')->item(0); + $spine = $this->meta_xpath->query('//opf:spine')->item(0); $tocid = $spine->getAttribute('toc'); - $tochref = $this->xpath->query('//opf:manifest/opf:item[@id="' . $tocid . '"]')->item(0)->attr('href'); + $tochref = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $tocid . '"]')->item(0)->attr('href'); $tocpath = $this->getFullPath($tochref); // read epub toc if (!$this->zip->FileExists($tocpath)) { @@ -88,11 +93,10 @@ public function initSpineComponent() } $data = $this->zip->FileRead($tocpath); - $this->toc = new \DOMDocument(); - $this->toc->registerNodeClass('DOMElement', '\\splitbrain\\epubmeta\\EPubDOMElement'); - $this->toc->loadXML($data); - $this->toc_xpath = new EPubDOMXPath($this->toc); - $rootNamespace = $this->toc->lookupNamespaceUri($this->toc->namespaceURI); + $this->toc_xml = new EPubDOMDocument(); + $this->toc_xml->loadXML($data); + $this->toc_xpath = new EPubDOMXPath($this->toc_xml); + $rootNamespace = $this->toc_xml->lookupNamespaceUri($this->toc_xml->namespaceURI); $this->toc_xpath->registerNamespace('x', $rootNamespace); } @@ -139,13 +143,13 @@ public function save() /** * Get the updated epub */ - public function download($file=false) + public function download($file = false) { - $this->zip->FileReplace($this->meta, $this->xml->saveXML()); + $this->zip->FileReplace($this->meta, $this->meta_xml->saveXML()); // add the cover image if ($this->imagetoadd) { $this->zip->FileReplace($this->coverpath, file_get_contents($this->imagetoadd)); - $this->imagetoadd=''; + $this->imagetoadd = ''; } if ($file) { $this->zip->Flush(TBSZIP_DOWNLOAD, $file); @@ -158,11 +162,11 @@ public function download($file=false) public function components() { $spine = array(); - $nodes = $this->xpath->query('//opf:spine/opf:itemref'); + $nodes = $this->meta_xpath->query('//opf:spine/opf:itemref'); foreach ($nodes as $node) { /** @var EPubDOMElement $node */ - $idref = $node->getAttribute('idref'); - $spine[] = $this->encodeComponentName($this->xpath->query('//opf:manifest/opf:item[@id="' . $idref . '"]')->item(0)->getAttribute('href')); + $idref = $node->getAttribute('idref'); + $spine[] = $this->encodeComponentName($this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $idref . '"]')->item(0)->getAttribute('href')); } return $spine; } @@ -205,8 +209,8 @@ public function getComponentName($comp, $elementPath) private function encodeComponentName($src) { return str_replace(array('/', '-'), - array('~SLASH~', '~DASH~'), - $src); + array('~SLASH~', '~DASH~'), + $src); } /** @@ -215,25 +219,24 @@ private function encodeComponentName($src) private function decodeComponentName($src) { return str_replace(array('~SLASH~', '~DASH~'), - array('/', '-'), - $src); + array('/', '-'), + $src); } - /** * Get the component content type */ public function componentContentType($comp) { $comp = $this->decodeComponentName($comp); - $item = $this->xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); + $item = $this->meta_xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); if ($item) { return $item->getAttribute('media-type'); } // I had at least one book containing %20 instead of spaces in the opf file $comp = str_replace(' ', '%20', $comp); - $item = $this->xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); + $item = $this->meta_xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); if ($item) { return $item->getAttribute('media-type'); } @@ -244,7 +247,7 @@ private function getNavPointDetail($node) { $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue; $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src'); - $src = $this->encodeComponentName ($src); + $src = $this->encodeComponentName($src); return array('title' => $title, 'src' => $src); } @@ -288,12 +291,13 @@ public function contents() * @param array|string|null $authors * @return array */ - public function Authors($authors = null) { + public function Authors($authors = null) + { // set new data - if($authors !== null) { + if ($authors !== null) { // Author where given as a comma separated list - if(is_string($authors)) { - if($authors == '') { + if (is_string($authors)) { + if ($authors == '') { $authors = array(); } else { $authors = explode(',', $authors); @@ -302,16 +306,16 @@ public function Authors($authors = null) { } // delete existing nodes - $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); - foreach($nodes as $node) { + $nodes = $this->meta_xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); + foreach ($nodes as $node) { /** @var EPubDOMElement $node */ $node->delete(); } // add new nodes - $parent = $this->xpath->query('//opf:metadata')->item(0); - foreach($authors as $as => $name) { - if(is_int($as)) { + $parent = $this->meta_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); @@ -325,20 +329,20 @@ public function Authors($authors = null) { // read current data $rolefix = false; $authors = array(); - $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); - if($nodes->length == 0) { + $nodes = $this->meta_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'); + $nodes = $this->meta_xpath->query('//opf:metadata/dc:creator'); $rolefix = true; } - foreach($nodes as $node) { + foreach ($nodes as $node) { $name = $node->nodeValue; $as = $node->attr('opf:file-as'); - if(!$as) { + if (!$as) { $as = $name; $node->attr('opf:file-as', $as); } - if($rolefix) { + if ($rolefix) { $node->attr('opf:role', 'aut'); } $authors[$as] = $name; @@ -352,7 +356,8 @@ public function Authors($authors = null) { * @param string|null $title * @return string */ - public function Title($title = null) { + public function Title($title = null) + { return $this->getset('dc:title', $title); } @@ -362,7 +367,8 @@ public function Title($title = null) { * @param string|null $lang * @return string */ - public function Language($lang = null) { + public function Language($lang = null) + { return $this->getset('dc:language', $lang); } @@ -372,7 +378,8 @@ public function Language($lang = null) { * @param string|null $publisher * @return string */ - public function Publisher($publisher = null) { + public function Publisher($publisher = null) + { return $this->getset('dc:publisher', $publisher); } @@ -382,7 +389,8 @@ public function Publisher($publisher = null) { * @param string|null $rights * @return string */ - public function Copyright($rights = null) { + public function Copyright($rights = null) + { return $this->getset('dc:rights', $rights); } @@ -392,7 +400,8 @@ public function Copyright($rights = null) { * @param string|null $description * @return string */ - public function Description($description = null) { + public function Description($description = null) + { return $this->getset('dc:description', $description); } @@ -404,9 +413,10 @@ public function Description($description = null) { * @throws \Exception * @todo auto add unique identifer if needed */ - public function Uuid($uuid = null) { - $nodes = $this->xpath->query('/opf:package'); - if($nodes->length !== 1) { + public function Uuid($uuid = null) + { + $nodes = $this->meta_xpath->query('/opf:package'); + if ($nodes->length !== 1) { throw new \Exception('Cannot find ebook identifier'); } $identifier = $nodes->item(0)->attr('unique-identifier'); @@ -423,7 +433,8 @@ public function Uuid($uuid = null) { * @todo use DateTime class instead of string * @return string */ - public function CreationDate($date = null) { + public function CreationDate($date = null) + { $res = $this->getset('dc:date', $date, 'opf:event', 'creation'); return $res; @@ -436,7 +447,8 @@ public function CreationDate($date = null) { * @todo use DateTime class instead of string * @return string */ - public function ModificationDate($date = null) { + public function ModificationDate($date = null) + { $res = $this->getset('dc:date', $date, 'opf:event', 'modification'); return $res; @@ -448,7 +460,8 @@ public function ModificationDate($date = null) { * @param string|null $uri URI * @return string */ - public function Uri($uri = null) { + public function Uri($uri = null) + { $res = $this->getset('dc:identifier', $uri, 'opf:scheme', 'URI'); return $res; @@ -460,7 +473,8 @@ public function Uri($uri = null) { * @param string|null $isbn * @return string */ - public function ISBN($isbn = null) { + public function ISBN($isbn = null) + { return $this->getset('dc:identifier', $isbn, 'opf:scheme', 'ISBN'); } @@ -470,7 +484,8 @@ public function ISBN($isbn = null) { * @param string|null $google * @return string */ - public function Google($google = null) { + public function Google($google = null) + { return $this->getset('dc:identifier', $google, 'opf:scheme', 'GOOGLE'); } @@ -480,7 +495,8 @@ public function Google($google = null) { * @param string|null $amazon * @return string */ - public function Amazon($amazon = null) { + public function Amazon($amazon = null) + { return $this->getset('dc:identifier', $amazon, 'opf:scheme', 'AMAZON'); } @@ -490,7 +506,8 @@ public function Amazon($amazon = null) { * @param null|string $uuid * @return string */ - public function Calibre($uuid = null) { + public function Calibre($uuid = null) + { return $this->getset('dc:identifier', $uuid, 'opf:scheme', 'calibre'); } @@ -500,7 +517,8 @@ public function Calibre($uuid = null) { * @param string|null $series * @return string */ - public function Series($series = null) { + public function Series($series = null) + { return $this->getset('opf:meta', $series, 'name', 'calibre:series', 'content'); } @@ -510,7 +528,8 @@ public function Series($series = null) { * @param string|null $seriesIndex * @return string */ - public function SeriesIndex($seriesIndex = null) { + public function SeriesIndex($seriesIndex = null) + { return $this->getset('opf:meta', $seriesIndex, 'name', 'calibre:series_index', 'content'); } @@ -523,11 +542,12 @@ public function SeriesIndex($seriesIndex = null) { * @param array|string|null $subjects * @return string[] */ - public function Subjects($subjects = null) { + public function Subjects($subjects = null) + { // setter - if($subjects !== null) { - if(is_string($subjects)) { - if($subjects === '') { + if ($subjects !== null) { + if (is_string($subjects)) { + if ($subjects === '') { $subjects = array(); } else { $subjects = explode(',', $subjects); @@ -536,15 +556,15 @@ public function Subjects($subjects = null) { } // delete previous - $nodes = $this->xpath->query('//opf:metadata/dc:subject'); - foreach($nodes as $node) { + $nodes = $this->meta_xpath->query('//opf:metadata/dc:subject'); + foreach ($nodes as $node) { /** @var EPubDOMElement $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)); + $parent = $this->meta_xpath->query('//opf:metadata')->item(0); + foreach ($subjects as $subj) { + $node = $this->meta_xml->createElement('dc:subject', htmlspecialchars($subj)); $parent->appendChild($node); } @@ -553,8 +573,8 @@ public function Subjects($subjects = null) { //getter $subjects = array(); - $nodes = $this->xpath->query('//opf:metadata/dc:subject'); - foreach($nodes as $node) { + $nodes = $this->meta_xpath->query('//opf:metadata/dc:subject'); + foreach ($nodes as $node) { $subjects[] = $node->nodeValue; } return $subjects; @@ -582,18 +602,18 @@ public function Subjects($subjects = null) { * @param string $mime mime type of the given file * @return array */ - public function Cover($path=false, $mime=false) + 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"]'); + $nodes = $this->meta_xpath->query('//opf:metadata/opf:meta[@name="cover"]'); foreach ($nodes as $node) { /** @var EPubDOMElement $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"]'); + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="php-epub-meta-cover"]'); foreach ($nodes as $node) { /** @var EPubDOMElement $node */ $node->delete(); @@ -601,13 +621,13 @@ public function Cover($path=false, $mime=false) if ($path) { // add pointer - $parent = $this->xpath->query('//opf:metadata')->item(0); + $parent = $this->meta_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); + $parent = $this->meta_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'); @@ -621,16 +641,16 @@ public function Cover($path=false, $mime=false) } // load cover - $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + $nodes = $this->meta_xpath->query('//opf:metadata/opf:meta[@name="cover"]'); if (!$nodes->length) { return $this->no_cover(); } - $coverid = (String) $nodes->item(0)->attr('opf:content'); + $coverid = (String)$nodes->item(0)->attr('opf:content'); if (!$coverid) { return $this->no_cover(); } - $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]'); + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]'); if (!$nodes->length) { return $this->no_cover(); } @@ -646,27 +666,27 @@ public function Cover($path=false, $mime=false) $data = $zip->getFromName($path); return array( - 'mime' => $mime, - 'data' => $data, + 'mime' => $mime, + 'data' => $data, 'found' => $path ); } public function getCoverItem() { - $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + $nodes = $this->meta_xpath->query('//opf:metadata/opf:meta[@name="cover"]'); if (!$nodes->length) { - return NULL; + return null; } $coverid = (String)$nodes->item(0)->attr('opf:content'); if (!$coverid) { - return NULL; + return null; } - $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]'); + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]'); if (!$nodes->length) { - return NULL; + return null; } return $nodes->item(0); @@ -704,13 +724,13 @@ public function combine($a, $b) $path = implode('/', $pathParts); if ($isAbsolute) { - return('/' . $path); + return ('/' . $path); } else { - return($path); + return ($path); } } - private function getFullPath($file, $context = NULL) + private function getFullPath($file, $context = null) { $path = dirname('/' . $this->meta) . '/' . $file; $path = ltrim($path, '\\'); @@ -730,7 +750,7 @@ public function updateForKepub() } } - public function Cover2($path=false, $mime=false) + public function Cover2($path = false, $mime = false) { $hascover = true; $item = $this->getCoverItem(); @@ -777,12 +797,12 @@ public function Cover2($path=false, $mime=false) * @param string|null $datt Destination attribute to set instead of the node value * @return string */ - protected function getset($item, $value=null, $att=null, $aval=null, $datt=null) + protected function getset($item, $value = null, $att = null, $aval = null, $datt = null) { // construct xpath $xpath = '//opf:metadata/' . $item; if ($att) { - if($aval) { + if ($aval) { $xpath .= '[@' . $att . '="' . $aval . '"]'; } else { $xpath .= '[@' . $att . ']'; @@ -792,8 +812,8 @@ protected function getset($item, $value=null, $att=null, $aval=null, $datt=null) // set value if ($value !== null) { $value = htmlspecialchars($value); - $nodes = $this->xpath->query($xpath); - if ($nodes->length == 1 ) { + $nodes = $this->meta_xpath->query($xpath); + if ($nodes->length == 1) { if ($value === '') { // the user want's to empty this value -> delete the node $nodes->item(0)->delete(); @@ -814,7 +834,7 @@ protected function getset($item, $value=null, $att=null, $aval=null, $datt=null) } // readd them if ($value) { - $parent = $this->xpath->query('//opf:metadata')->item(0); + $parent = $this->meta_xpath->query('//opf:metadata')->item(0); $node = $parent->newChild($item); /** @var EPubDOMElement $node */ @@ -833,7 +853,7 @@ protected function getset($item, $value=null, $att=null, $aval=null, $datt=null) } // get value - $nodes = $this->xpath->query($xpath); + $nodes = $this->meta_xpath->query($xpath); if ($nodes->length) { if ($datt) { return $nodes->item(0)->attr($datt); @@ -851,8 +871,8 @@ protected function getset($item, $value=null, $att=null, $aval=null, $datt=null) protected function no_cover() { return array( - 'data' => base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'), - 'mime' => 'image/gif', + 'data' => base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'), + 'mime' => 'image/gif', 'found' => false ); } @@ -865,7 +885,7 @@ protected function no_cover() */ protected function reparse() { - $this->xml->loadXML($this->xml->saveXML()); - $this->xpath = new EPubDOMXPath($this->xml); + $this->meta_xml->loadXML($this->meta_xml->saveXML()); + $this->meta_xpath = new EPubDOMXPath($this->meta_xml); } } diff --git a/lib/EPubDOMDocument.php b/lib/EPubDOMDocument.php new file mode 100644 index 0000000..a65c58c --- /dev/null +++ b/lib/EPubDOMDocument.php @@ -0,0 +1,23 @@ +<?php + +namespace splitbrain\epubmeta; + +class EpubDOMDocument extends \DOMDocument +{ + + /** @var EPubDOMElement documentElement */ + public $documentElement; + + /** + * Creates a new DOMDocument object + * @link http://php.net/manual/domdocument.construct.php + * @param $version [optional] The version number of the document as part of the XML declaration. + * @param $encoding [optional] The encoding of the document as part of the XML declaration. + */ + public function __construct($version = '', $encoding = '') + { + parent::__construct($version, $encoding); + $this->registerNodeClass('DOMElement', '\\splitbrain\\epubmeta\\EPubDOMElement'); + } + +} \ No newline at end of file diff --git a/lib/EPubDOMElement.php b/lib/EPubDOMElement.php index 28711c8..d40e54f 100644 --- a/lib/EPubDOMElement.php +++ b/lib/EPubDOMElement.php @@ -11,12 +11,12 @@ class EPubDOMElement extends \DOMElement { public $namespaces = array( - 'n' => 'urn:oasis:names:tc:opendocument:xmlns:container', + 'n' => 'urn:oasis:names:tc:opendocument:xmlns:container', 'opf' => 'http://www.idpf.org/2007/opf', - 'dc' => 'http://purl.org/dc/elements/1.1/', + 'dc' => 'http://purl.org/dc/elements/1.1/', ); - public function __construct($name, $value='', $namespaceURI='') + public function __construct($name, $value = '', $namespaceURI = '') { list($ns, $name) = $this->splitns($name); $value = htmlspecialchars($value); @@ -34,14 +34,14 @@ public function __construct($name, $value='', $namespaceURI='') * @param string $value * @return \DOMNode */ - public function newChild($name, $value='') + public function newChild($name, $value = '') { $nsuri = ''; list($ns, $local) = $this->splitns($name); if ($ns) { $nsuri = $this->namespaces[$ns]; if ($this->isDefaultNamespace($nsuri)) { - $name = $local; + $name = $local; $nsuri = ''; } } @@ -69,7 +69,7 @@ public function splitns($name) /** * Simple EPub namespace aware attribute accessor */ - public function attr($attr, $value=null) + public function attr($attr, $value = null) { list($ns, $attr) = $this->splitns($attr); diff --git a/lib/EPubDOMNodeList.php b/lib/EPubDOMNodeList.php index 32502d1..eda3a40 100644 --- a/lib/EPubDOMNodeList.php +++ b/lib/EPubDOMNodeList.php @@ -2,13 +2,15 @@ namespace splitbrain\epubmeta; -class EPubDOMNodeList extends \DOMNodeList { +class EPubDOMNodeList extends \DOMNodeList +{ /** * @link http://php.net/manual/en/domnodelist.item.php * @param int $index * @return EPubDOMElement */ - public function item($index) { + public function item($index) + { return parent::item($index); } diff --git a/lib/EPubDOMXPath.php b/lib/EPubDOMXPath.php index eb763ba..1e72bcb 100644 --- a/lib/EPubDOMXPath.php +++ b/lib/EPubDOMXPath.php @@ -10,15 +10,12 @@ class EPubDOMXPath extends \DOMXPath { - public function __construct(\DOMDocument $doc) + public function __construct(EpubDOMDocument $doc) { parent::__construct($doc); - if (is_a($doc->documentElement, '\\splitbrain\\epubmeta\\EPubDOMElement')) - { - foreach ($doc->documentElement->namespaces as $ns => $url) { - $this->registerNamespace($ns, $url); - } + foreach ($doc->documentElement->namespaces as $ns => $url) { + $this->registerNamespace($ns, $url); } } @@ -29,7 +26,8 @@ public function __construct(\DOMDocument $doc) * @param \DOMNode $contextnode * @return EpubDOMNodeList */ - public function query($expression, $contextnode = null) { + public function query($expression, $contextnode = null) + { return parent::query($expression, $contextnode); } diff --git a/test/Epub.php b/test/Epub.php new file mode 100644 index 0000000..5393625 --- /dev/null +++ b/test/Epub.php @@ -0,0 +1,15 @@ +<?php + +namespace splitbrain\epubmeta\test; + +/** + * Class EPub + * + * Gives access to protected methods for testing + * + * @package splitbrain\epubmeta\test + */ +class EPub extends \splitbrain\epubmeta\EPub +{ + +} \ No newline at end of file diff --git a/test/epubTest.php b/test/epubTest.php index 0613c4f..2ef2126 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -4,30 +4,33 @@ use splitbrain\epubmeta\EPub; -class EPubTest extends \PHPUnit_Framework_TestCase { +class EPubTest extends \PHPUnit_Framework_TestCase +{ /** @var EPub */ protected $epub; - protected function setUp(){ + protected function setUp() + { // sometime I might have accidentally broken the test file - if(filesize(realpath( dirname( __FILE__ ) ) . '/test.epub') != 768780){ + if (filesize(realpath(dirname(__FILE__)) . '/test.epub') != 768780) { die('test.epub has wrong size, make sure it\'s unmodified'); } // we work on a copy to test saving - if(!copy(realpath( dirname( __FILE__ ) ) . '/test.epub', realpath( dirname( __FILE__ ) ) . '/test.copy.epub')){ + if (!copy(realpath(dirname(__FILE__)) . '/test.epub', realpath(dirname(__FILE__)) . '/test.copy.epub')) { die('failed to create copy of the test book'); } - $this->epub = new EPub(realpath( dirname( __FILE__ ) ) . '/test.copy.epub'); + $this->epub = new EPub(realpath(dirname(__FILE__)) . '/test.copy.epub'); } public static function tearDownAfterClass() { - unlink(realpath( dirname( __FILE__ ) ) . '/test.copy.epub'); + unlink(realpath(dirname(__FILE__)) . '/test.copy.epub'); } - public function testAuthors(){ + public function testAuthors() + { // read curent value $this->assertEquals( array('Shakespeare, William' => 'William Shakespeare'), @@ -90,7 +93,8 @@ public function testAuthors(){ ); } - public function testTitle(){ + public function testTitle() + { // get current value $this->assertEquals( 'Romeo and Juliet', @@ -122,10 +126,11 @@ public function testTitle(){ ); } - public function testSubject(){ + public function testSubject() + { // get current values $this->assertEquals( - array('Fiction','Drama','Romance'), + array('Fiction', 'Drama', 'Romance'), $this->epub->Subjects() ); @@ -137,7 +142,7 @@ public function testSubject(){ // set new values with String $this->assertEquals( - array('Fiction','Drama','Romance'), + array('Fiction', 'Drama', 'Romance'), $this->epub->Subjects('Fiction, Drama, Romance') ); @@ -149,18 +154,17 @@ public function testSubject(){ // set new values with array $this->assertEquals( - array('Fiction','Drama','Romance'), - $this->epub->Subjects(array('Fiction','Drama','Romance')) + array('Fiction', 'Drama', 'Romance'), + $this->epub->Subjects(array('Fiction', 'Drama', 'Romance')) ); // check escaping $this->assertEquals( - array('Fiction','Drama ','Romance'), - $this->epub->Subjects(array('Fiction','Drama ','Romance')) + array('Fiction', 'Drama ', 'Romance'), + $this->epub->Subjects(array('Fiction', 'Drama ', 'Romance')) ); } - /*public function testCover(){ // read current cover $cover = $this->epub->Cover2(); From 9aa467b63a1ca69b4bf29b0df2ade69507dc6ba4 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sun, 10 Jul 2016 00:50:57 +0200 Subject: [PATCH 50/63] simplify TOC and file management --- composer.json | 1 + composer.lock | 2 +- lib/EPub.php | 268 +++++++++++++++++------------------- lib/EPubDOMElement.php | 12 +- lib/EPubDOMXPath.php | 2 +- test/{Epub.php => EPub.php} | 5 + test/epubTest.php | 36 ++++- 7 files changed, 180 insertions(+), 146 deletions(-) rename test/{Epub.php => EPub.php} (70%) diff --git a/composer.json b/composer.json index 9d33bfe..b1551bb 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ }, "autoload": { "psr-4": { + "splitbrain\\epubmeta\\test\\": "test", "splitbrain\\epubmeta\\": "lib" } } diff --git a/composer.lock b/composer.lock index 0532e45..9bb8ca6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "924b40829f78ea2af3de2fc516d36709", + "hash": "bf907b849328209271870729fe288382", "content-hash": "d0841a1d14a8e6ea6305431ea76f1267", "packages": [ { diff --git a/lib/EPub.php b/lib/EPub.php index 5eb7dbc..589205e 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -20,11 +20,6 @@ class EPub /** @var EPubDOMXPath XPath access to the meta package */ protected $meta_xpath; - /** @var EPubDOMDocument The Table of Contents file */ - public $toc_xml; - /** @var EPubDOMXPath XPath access to the TOC */ - protected $toc_xpath; - /** @var string The path to the epub file */ protected $file; @@ -34,6 +29,9 @@ class EPub protected $namespaces; protected $imagetoadd = ''; + /** @var null|array The manifest data, eg. which files are available */ + protected $manifest = null; + /** * Constructor * @@ -80,26 +78,6 @@ public function __construct($file, $zipClass = 'clsTbsZip') $this->meta_xpath = new EPubDOMXPath($this->meta_xml); } - public function initSpineComponent() - { - /** @var EPubDOMElement $spine */ - $spine = $this->meta_xpath->query('//opf:spine')->item(0); - $tocid = $spine->getAttribute('toc'); - $tochref = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $tocid . '"]')->item(0)->attr('href'); - $tocpath = $this->getFullPath($tochref); - // read epub toc - if (!$this->zip->FileExists($tocpath)) { - throw new \Exception('Unable to find ' . $tocpath); - } - - $data = $this->zip->FileRead($tocpath); - $this->toc_xml = new EPubDOMDocument(); - $this->toc_xml->loadXML($data); - $this->toc_xpath = new EPubDOMXPath($this->toc_xml); - $rootNamespace = $this->toc_xml->lookupNamespaceUri($this->toc_xml->namespaceURI); - $this->toc_xpath->registerNamespace('x', $rootNamespace); - } - /** * file name getter */ @@ -156,120 +134,7 @@ public function download($file = false) } } - /** - * Get the components list as an array - */ - public function components() - { - $spine = array(); - $nodes = $this->meta_xpath->query('//opf:spine/opf:itemref'); - foreach ($nodes as $node) { - /** @var EPubDOMElement $node */ - $idref = $node->getAttribute('idref'); - $spine[] = $this->encodeComponentName($this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $idref . '"]')->item(0)->getAttribute('href')); - } - return $spine; - } - /** - * Get the component content - */ - public function component($comp) - { - $path = $this->decodeComponentName($comp); - $path = $this->getFullPath($path); - if (!$this->zip->FileExists($path)) { - throw new \Exception('Unable to find ' . $path . ' <' . $comp . '>'); - } - - $data = $this->zip->FileRead($path); - return $data; - } - - public function getComponentName($comp, $elementPath) - { - $path = $this->decodeComponentName($comp); - $path = $this->getFullPath($path, $elementPath); - if (!$this->zip->FileExists($path)) { - error_log('Unable to find ' . $path); - return false; - } - $ref = dirname('/' . $this->meta); - $ref = ltrim($ref, '\\'); - $ref = ltrim($ref, '/'); - if (strlen($ref) > 0) { - $path = str_replace($ref . '/', '', $path); - } - return $this->encodeComponentName($path); - } - - /** - * Encode the component name (to replace / and -) - */ - private function encodeComponentName($src) - { - return str_replace(array('/', '-'), - array('~SLASH~', '~DASH~'), - $src); - } - - /** - * Decode the component name (to replace / and -) - */ - private function decodeComponentName($src) - { - return str_replace(array('~SLASH~', '~DASH~'), - array('/', '-'), - $src); - } - - /** - * Get the component content type - */ - public function componentContentType($comp) - { - $comp = $this->decodeComponentName($comp); - $item = $this->meta_xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); - if ($item) { - return $item->getAttribute('media-type'); - } - - // I had at least one book containing %20 instead of spaces in the opf file - $comp = str_replace(' ', '%20', $comp); - $item = $this->meta_xpath->query('//opf:manifest/opf:item[@href="' . $comp . '"]')->item(0); - if ($item) { - return $item->getAttribute('media-type'); - } - return 'application/octet-stream'; - } - - private function getNavPointDetail($node) - { - $title = $this->toc_xpath->query('x:navLabel/x:text', $node)->item(0)->nodeValue; - $src = $this->toc_xpath->query('x:content', $node)->item(0)->attr('src'); - $src = $this->encodeComponentName($src); - return array('title' => $title, 'src' => $src); - } - - /** - * Get the Epub content (TOC) as an array - * - * For each chapter there is a "title" and a "src" - */ - public function contents() - { - $contents = array(); - $nodes = $this->toc_xpath->query('//x:ncx/x:navMap/x:navPoint'); - foreach ($nodes as $node) { - $contents[] = $this->getNavPointDetail($node); - - $insidenodes = $this->toc_xpath->query('x:navPoint', $node); - foreach ($insidenodes as $insidenode) { - $contents[] = $this->getNavPointDetail($insidenode); - } - } - return $contents; - } #region Book Attribute Getter/Setters @@ -582,6 +447,131 @@ public function Subjects($subjects = null) #endregion + #region Book Attribute Getters + + /** + * Lists all files from the manifest + * + * @return array + */ + protected function readManifest() + { + $manifest = array(); + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item'); + + foreach ($nodes as $node) { + /** @var EPubDOMElement $node */ + $file = $node->attr('opf:href'); + if ($file === '') { + continue; + } + $file = $this->getFullPath($file); + + $manifest[$file] = array( + 'id' => $node->attr('id'), + 'mime' => $node->attr('opf:media-type'), + 'exists' => (bool)$this->zip->FileExists($file), + 'path' => $file + ); + } + + return $manifest; + } + + /** + * Returns info about the given file from the manifest + * + * @param $path + * @return array + */ + public function getFileInfo($path) + { + if ($this->manifest === null) { + $this->manifest = $this->readManifest(); + } + + if (isset($this->manifest[$path])) { + return $this->manifest[$path]; + } + return array('id' => '', 'mime' => '', 'exists' => false, 'file' => $path); + } + + /** + * Returns the Table of Contents + * + * @return array + * @throws \Exception + */ + public function readTOC() + { + $contents = array(); + + // find TOC file + $tocid = $this->meta_xpath->query('//opf:spine')->item(0)->attr('toc'); + $tochref = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $tocid . '"]')->item(0)->attr('href'); + $tocpath = $this->getFullPath($tochref); + // read TOC file + if (!$this->zip->FileExists($tocpath)) { + throw new \Exception('Unable to find ' . $tocpath); + } + $data = $this->zip->FileRead($tocpath); + // parse TOC file + $toc_xml = new EPubDOMDocument(); + $toc_xml->loadXML($data); + $toc_xpath = new EPubDOMXPath($toc_xml); + + // read nav point nodes + $nodes = $toc_xpath->query('//ncx:ncx/ncx:navMap/ncx:navPoint'); + foreach ($nodes as $node) { + $contents[] = $this->mkTocEntry($node, $toc_xpath); + + $insidenodes = $toc_xpath->query('ncx:navPoint', $node); + foreach ($insidenodes as $insidenode) { + $contents[] = $this->mkTocEntry($insidenode, $toc_xpath); + } + } + + return $contents; + } + + /** + * Enhances the a single TOC entry with data from the manifest + * + * @param EPubDOMElement $node an ncx:navPoint entry + * @param EPubDOMXPath $toc_xpath + * @return array + */ + protected function mkTocEntry(EPubDOMElement $node, EPubDOMXPath $toc_xpath) + { + $title = $toc_xpath->query('ncx:navLabel/ncx:text', $node)->item(0)->nodeValue; + $src = $toc_xpath->query('ncx:content', $node)->item(0)->attr('src'); + + $file = $this->getFullPath($src); + + return array_merge(array('title' => $title, 'src' => $src), $this->getFileInfo($file)); + } + + /** + * Read the contents of a file witin the epub + * + * You probably want to use getFileInfo() first to check if the file exists and get + * additional file info like the mime type + * + * @param string $path the path within the epub file + * @return string the raw file contents + * @throws \Exception when the file doesn't exists + */ + public function getFile($path) + { + if (!$this->zip->FileExists($path)) { + throw new \Exception('No such file'); + } + + return $this->zip->FileRead($path); + } + + #endregion + /** * Read the cover data * @@ -732,6 +722,8 @@ public function combine($a, $b) private function getFullPath($file, $context = null) { + list($file) = explode('#', $file); // strip anchors + $path = dirname('/' . $this->meta) . '/' . $file; $path = ltrim($path, '\\'); $path = ltrim($path, '/'); diff --git a/lib/EPubDOMElement.php b/lib/EPubDOMElement.php index d40e54f..6209a40 100644 --- a/lib/EPubDOMElement.php +++ b/lib/EPubDOMElement.php @@ -10,10 +10,11 @@ class EPubDOMElement extends \DOMElement { - public $namespaces = array( + static 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/', + 'ncx' => 'http://www.daisy.org/z3986/2005/ncx/' ); public function __construct($name, $value = '', $namespaceURI = '') @@ -21,7 +22,7 @@ public function __construct($name, $value = '', $namespaceURI = '') list($ns, $name) = $this->splitns($name); $value = htmlspecialchars($value); if (!$namespaceURI && $ns) { - $namespaceURI = $this->namespaces[$ns]; + $namespaceURI = self::$namespaces[$ns]; } parent::__construct($name, $value, $namespaceURI); } @@ -39,7 +40,7 @@ public function newChild($name, $value = '') $nsuri = ''; list($ns, $local) = $this->splitns($name); if ($ns) { - $nsuri = $this->namespaces[$ns]; + $nsuri = self::$namespaces[$ns]; if ($this->isDefaultNamespace($nsuri)) { $name = $local; $nsuri = ''; @@ -68,6 +69,9 @@ public function splitns($name) /** * Simple EPub namespace aware attribute accessor + * @param string $attr Attribute to access + * @param null $value Value to set. False to delete + * @return string */ public function attr($attr, $value = null) { @@ -75,7 +79,7 @@ public function attr($attr, $value = null) $nsuri = ''; if ($ns) { - $nsuri = $this->namespaces[$ns]; + $nsuri = self::$namespaces[$ns]; if (!$this->namespaceURI) { if ($this->isDefaultNamespace($nsuri)) { $nsuri = ''; diff --git a/lib/EPubDOMXPath.php b/lib/EPubDOMXPath.php index 1e72bcb..f61989f 100644 --- a/lib/EPubDOMXPath.php +++ b/lib/EPubDOMXPath.php @@ -14,7 +14,7 @@ public function __construct(EpubDOMDocument $doc) { parent::__construct($doc); - foreach ($doc->documentElement->namespaces as $ns => $url) { + foreach (EPubDOMElement::$namespaces as $ns => $url) { $this->registerNamespace($ns, $url); } } diff --git a/test/Epub.php b/test/EPub.php similarity index 70% rename from test/Epub.php rename to test/EPub.php index 5393625..618f6b3 100644 --- a/test/Epub.php +++ b/test/EPub.php @@ -12,4 +12,9 @@ class EPub extends \splitbrain\epubmeta\EPub { + public function readManifest() + { + return parent::readManifest(); + } + } \ No newline at end of file diff --git a/test/epubTest.php b/test/epubTest.php index 2ef2126..461480c 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -2,8 +2,6 @@ namespace splitbrain\epubmeta\test; -use splitbrain\epubmeta\EPub; - class EPubTest extends \PHPUnit_Framework_TestCase { /** @var EPub */ @@ -29,6 +27,40 @@ public static function tearDownAfterClass() unlink(realpath(dirname(__FILE__)) . '/test.copy.epub'); } + public function testManifest() + { + $manifest = $this->epub->readManifest(); + + $this->assertEquals(41, count($manifest)); + $this->assertArrayHasKey('OPS/css/page.css', $manifest); + $this->assertEquals( + array( + 'id' => 'page-css', + 'mime' => 'text/css', + 'exists' => true, + 'path' => 'OPS/css/page.css' + ), + $manifest['OPS/css/page.css'] + ); + } + + public function testToc() + { + $toc = $this->epub->readTOC(); + + $this->assertEquals(34, count($toc)); + $this->assertEquals( + array( + 'title' => 'Prologue', + 'src' => 'main0.xml#section_77304', + 'id' => 'main0', + 'mime' => 'application/xhtml+xml', + 'exists' => true, + 'path' => 'OPS/main0.xml' + ) + , $toc[3]); + } + public function testAuthors() { // read curent value From c92e2ffb14eccbe18328c0ee9f52f8210c3d1f94 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sun, 10 Jul 2016 18:44:01 +0200 Subject: [PATCH 51/63] file reading and tests --- lib/EPub.php | 116 +++++++++++++++++++++++++--------------------- test/epubTest.php | 36 +++++++++++++- 2 files changed, 97 insertions(+), 55 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index 589205e..df372f8 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -79,13 +79,36 @@ public function __construct($file, $zipClass = 'clsTbsZip') } /** - * file name getter + * Lists all files from the manifest + * + * @return array */ - public function file() + protected function readManifest() { - return $this->file; + $manifest = array(); + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item'); + + foreach ($nodes as $node) { + /** @var EPubDOMElement $node */ + $file = $node->attr('opf:href'); + if ($file === '') { + continue; + } + $file = $this->getFullPath($file); + + $manifest[$file] = array( + 'id' => $node->attr('id'), + 'mime' => $node->attr('opf:media-type'), + 'exists' => (bool)$this->zip->FileExists($file), + 'path' => $file + ); + } + + return $manifest; } + + /** * Close the epub file */ @@ -450,51 +473,13 @@ public function Subjects($subjects = null) #region Book Attribute Getters /** - * Lists all files from the manifest - * - * @return array + * The path to the currently loaded EPub */ - protected function readManifest() + public function getEPubLocation() { - $manifest = array(); - $nodes = $this->meta_xpath->query('//opf:manifest/opf:item'); - - foreach ($nodes as $node) { - /** @var EPubDOMElement $node */ - $file = $node->attr('opf:href'); - if ($file === '') { - continue; - } - $file = $this->getFullPath($file); - - $manifest[$file] = array( - 'id' => $node->attr('id'), - 'mime' => $node->attr('opf:media-type'), - 'exists' => (bool)$this->zip->FileExists($file), - 'path' => $file - ); - } - - return $manifest; + return $this->file; } - /** - * Returns info about the given file from the manifest - * - * @param $path - * @return array - */ - public function getFileInfo($path) - { - if ($this->manifest === null) { - $this->manifest = $this->readManifest(); - } - - if (isset($this->manifest[$path])) { - return $this->manifest[$path]; - } - return array('id' => '', 'mime' => '', 'exists' => false, 'file' => $path); - } /** * Returns the Table of Contents @@ -502,7 +487,7 @@ public function getFileInfo($path) * @return array * @throws \Exception */ - public function readTOC() + public function getToc() { $contents = array(); @@ -535,20 +520,21 @@ public function readTOC() } /** - * Enhances the a single TOC entry with data from the manifest + * Returns info about the given file from the manifest * - * @param EPubDOMElement $node an ncx:navPoint entry - * @param EPubDOMXPath $toc_xpath + * @param $path * @return array */ - protected function mkTocEntry(EPubDOMElement $node, EPubDOMXPath $toc_xpath) + public function getFileInfo($path) { - $title = $toc_xpath->query('ncx:navLabel/ncx:text', $node)->item(0)->nodeValue; - $src = $toc_xpath->query('ncx:content', $node)->item(0)->attr('src'); - - $file = $this->getFullPath($src); + if ($this->manifest === null) { + $this->manifest = $this->readManifest(); + } - return array_merge(array('title' => $title, 'src' => $src), $this->getFileInfo($file)); + if (isset($this->manifest[$path])) { + return $this->manifest[$path]; + } + return array('id' => '', 'mime' => '', 'exists' => false, 'file' => $path); } /** @@ -572,6 +558,28 @@ public function getFile($path) #endregion + + #region Internal Functions + + /** + * Enhances the a single TOC entry with data from the manifest + * + * @param EPubDOMElement $node an ncx:navPoint entry + * @param EPubDOMXPath $toc_xpath + * @return array + */ + protected function mkTocEntry(EPubDOMElement $node, EPubDOMXPath $toc_xpath) + { + $title = $toc_xpath->query('ncx:navLabel/ncx:text', $node)->item(0)->nodeValue; + $src = $toc_xpath->query('ncx:content', $node)->item(0)->attr('src'); + + $file = $this->getFullPath($src); + + return array_merge(array('title' => $title, 'src' => $src), $this->getFileInfo($file)); + } + + #endregion + /** * Read the cover data * diff --git a/test/epubTest.php b/test/epubTest.php index 461480c..789325e 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -46,7 +46,7 @@ public function testManifest() public function testToc() { - $toc = $this->epub->readTOC(); + $toc = $this->epub->getToc(); $this->assertEquals(34, count($toc)); $this->assertEquals( @@ -60,6 +60,40 @@ public function testToc() ) , $toc[3]); } + + public function testFileReading() + { + $expect = '@import "page.css"; + +body {padding: 0;} +div.aboutauthor {text-align: left;} + +div.also { + text-align: left; + padding-top: 5%;} + +a { + color: #000000; + text-decoration: none;} + +p { + margin-top: 0.0em; + margin-bottom: 0.0em; + text-indent: 1.0em; + text-align: justify;}'; + + $data = $this->epub->getFile('OPS/css/about.css'); + + $this->assertEquals($expect, $data); + } + + /** + * @expectedException \Exception + */ + public function testFileFailed() + { + $this->epub->getFile('does/not/exist'); + } public function testAuthors() { From 53d4bf36ee8cfc0a57e5f211f04eed9f705af5ce Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sun, 10 Jul 2016 20:37:54 +0200 Subject: [PATCH 52/63] get and clear cover --- lib/EPub.php | 88 +++++++++++++++++++++++++++++++++-------------- test/epubTest.php | 21 ++++++++++- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index df372f8..04bce2f 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -107,8 +107,6 @@ protected function readManifest() return $manifest; } - - /** * Close the epub file */ @@ -480,7 +478,6 @@ public function getEPubLocation() return $this->file; } - /** * Returns the Table of Contents * @@ -537,6 +534,66 @@ public function getFileInfo($path) return array('id' => '', 'mime' => '', 'exists' => false, 'file' => $path); } + /** + * Get info on the cover image if any + * + * Returns the same info as getFileInfo() for the cover image if any. Returns null + * if there's no cover image. + * + * @return array|null + */ + public function getCoverFile() + { + $nodes = $this->meta_xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + if (!$nodes->length) { + return null; + } + + $coverid = (String)$nodes->item(0)->attr('opf:content'); + if (!$coverid) { + return null; + } + + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]'); + if (!$nodes->length) { + return null; + } + + return $this->getFileInfo($this->getFullPath($nodes->item(0)->attr('href'))); + } + + /** + * Removes the cover image if there's one + */ + public function clearCover() + { + $cover = $this->getCoverFile(); + if ($cover === null) { + return; + } + + // remove current pointer + $nodes = $this->meta_xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + foreach ($nodes as $node) { + /** @var EPubDOMElement $node */ + $node->delete(); + } + // remove previous manifest entries if they where made by us + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="php-epub-meta-cover"]'); + foreach ($nodes as $node) { + /** @var EPubDOMElement $node */ + $node->delete(); + } + + // FIXME we should remove the actual file from the archive if it was added by us, + //but the zip library used does not support deleting files, yet + } + + public function setCoverFile($path) + { + + } + /** * Read the contents of a file witin the epub * @@ -558,7 +615,6 @@ public function getFile($path) #endregion - #region Internal Functions /** @@ -670,26 +726,6 @@ public function Cover($path = false, $mime = false) ); } - public function getCoverItem() - { - $nodes = $this->meta_xpath->query('//opf:metadata/opf:meta[@name="cover"]'); - if (!$nodes->length) { - return null; - } - - $coverid = (String)$nodes->item(0)->attr('opf:content'); - if (!$coverid) { - return null; - } - - $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $coverid . '"]'); - if (!$nodes->length) { - return null; - } - - return $nodes->item(0); - } - public function combine($a, $b) { $isAbsolute = false; @@ -744,7 +780,7 @@ private function getFullPath($file, $context = null) public function updateForKepub() { - $item = $this->getCoverItem(); + $item = $this->getCoverFile(); if (!is_null($item)) { $item->attr('opf:properties', 'cover-image'); } @@ -753,7 +789,7 @@ public function updateForKepub() public function Cover2($path = false, $mime = false) { $hascover = true; - $item = $this->getCoverItem(); + $item = $this->getCoverFile(); if (is_null($item)) { $hascover = false; } else { diff --git a/test/epubTest.php b/test/epubTest.php index 789325e..8f2bf00 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -60,7 +60,7 @@ public function testToc() ) , $toc[3]); } - + public function testFileReading() { $expect = '@import "page.css"; @@ -231,6 +231,25 @@ public function testSubject() ); } + public function testGetCoverFile() + { + $expect = array( + 'id' => 'book-cover', + 'mime' => 'image/png', + 'exists' => true, + 'path' => 'OPS/images/cover.png', + ); + $cover = $this->epub->getCoverFile(); + $this->assertNotNull($cover); + $this->assertEquals($expect, $cover); + } + + public function testClearCover() + { + $this->epub->clearCover(); + $this->assertNull($this->epub->getCoverFile()); + } + /*public function testCover(){ // read current cover $cover = $this->epub->Cover2(); From 34b9d8aec9ee1d1c60688f028b97b9deeaa0b9a7 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Tue, 12 Jul 2016 21:27:19 +0200 Subject: [PATCH 53/63] setting cover file --- lib/EPub.php | 42 ++++++++++++++++++++++++++++++++++++++++-- test/epubTest.php | 19 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index 04bce2f..e4005c3 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -531,7 +531,7 @@ public function getFileInfo($path) if (isset($this->manifest[$path])) { return $this->manifest[$path]; } - return array('id' => '', 'mime' => '', 'exists' => false, 'file' => $path); + return array('id' => '', 'mime' => '', 'exists' => false, 'path' => $path); } /** @@ -589,9 +589,46 @@ public function clearCover() //but the zip library used does not support deleting files, yet } - public function setCoverFile($path) + /** + * Set a new cover image + * + * @param string $path path to the image to set on the local file system + * @param string $mime mime type of that image (like 'image/jpeg') + * @throws \Exception when the given image can't be read + */ + public function setCoverFile($path, $mime) { + $this->clearCover(); + $data = file_get_contents($path); + if ($data === false) { + throw new \Exception("Couldn't load data from $path"); + } + + /** @var EPubDOMElement $node */ + + // add pointer + $parent = $this->meta_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->meta_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); + + $full = $this->getFullPath('php-epub-meta-cover.img'); + + // remember path for save action + if ($this->zip->FileExists($full)) { + $this->zip->FileReplace($full, $data); + } else { + $this->zip->FileAdd($full, $data); + } + $this->reparse(); } /** @@ -923,5 +960,6 @@ protected function reparse() { $this->meta_xml->loadXML($this->meta_xml->saveXML()); $this->meta_xpath = new EPubDOMXPath($this->meta_xml); + $this->manifest = null; } } diff --git a/test/epubTest.php b/test/epubTest.php index 8f2bf00..c31b917 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -250,6 +250,25 @@ public function testClearCover() $this->assertNull($this->epub->getCoverFile()); } + public function testSetCoverFile() + { + $this->epub->setCoverFile(__DIR__ . '/test.jpg', 'image/jpeg'); + $cover = $this->epub->getCoverFile(); + + $this->assertNotNull($cover); + + $this->assertEquals( + array( + 'id' => 'php-epub-meta-cover', + 'mime' => 'image/jpeg', + 'exists' => false, + 'path' => 'OPS/php-epub-meta-cover.img' + + ), + $cover + ); + } + /*public function testCover(){ // read current cover $cover = $this->epub->Cover2(); From 5d111d340ced520e06013b8fc03387e317e78ce8 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Tue, 12 Jul 2016 21:35:16 +0200 Subject: [PATCH 54/63] color phpunit output --- phpunit.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index cac584a..115f946 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,7 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd" bootstrap="./vendor/autoload.php" - verbose="true"> + verbose="true" + colors="true"> <filter> <whitelist processUncoveredFilesFromWhitelist="false"> <!-- this is the path of the files included in your clover report --> From a7333f2ab8262b8aff7255ef6f29b887aeca3f46 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Wed, 13 Jul 2016 22:22:56 +0200 Subject: [PATCH 55/63] fix saving --- lib/EPub.php | 24 +++++++++++++----------- test/epubTest.php | 27 ++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index e4005c3..d1dd5a3 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -112,8 +112,6 @@ protected function readManifest() */ public function close() { - $this->zip->FileCancelModif($this->meta); - // TODO: Add cancelation of cover image $this->zip->Close(); } @@ -131,27 +129,30 @@ public function cleanITunesCrap() } /** - * Writes back all meta data changes + * Writes back all changes to the epub */ public function save() { - $this->download(); + $data = $this->download(); $this->zip->Close(); + file_put_contents($this->getEPubLocation(), $data); } /** * Get the updated epub + * + * @param null|string $file + * @return string|bool */ - public function download($file = false) + public function download($file = null) { $this->zip->FileReplace($this->meta, $this->meta_xml->saveXML()); - // add the cover image - if ($this->imagetoadd) { - $this->zip->FileReplace($this->coverpath, file_get_contents($this->imagetoadd)); - $this->imagetoadd = ''; - } + if ($file) { - $this->zip->Flush(TBSZIP_DOWNLOAD, $file); + return $this->zip->Flush(TBSZIP_DOWNLOAD, $file); + } else { + echo $this->zip->Flush(TBSZIP_STRING); + return $this->zip->OutputSrc; // ugly but currently the only interface in the ZIP lib } } @@ -587,6 +588,7 @@ public function clearCover() // FIXME we should remove the actual file from the archive if it was added by us, //but the zip library used does not support deleting files, yet + } /** diff --git a/test/epubTest.php b/test/epubTest.php index c31b917..57c33d1 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -10,16 +10,16 @@ class EPubTest extends \PHPUnit_Framework_TestCase protected function setUp() { // sometime I might have accidentally broken the test file - if (filesize(realpath(dirname(__FILE__)) . '/test.epub') != 768780) { + if (filesize(realpath(__DIR__) . '/test.epub') != 768780) { die('test.epub has wrong size, make sure it\'s unmodified'); } // we work on a copy to test saving - if (!copy(realpath(dirname(__FILE__)) . '/test.epub', realpath(dirname(__FILE__)) . '/test.copy.epub')) { + if (!copy(realpath(__DIR__) . '/test.epub', realpath(__DIR__) . '/test.copy.epub')) { die('failed to create copy of the test book'); } - $this->epub = new EPub(realpath(dirname(__FILE__)) . '/test.copy.epub'); + $this->epub = new EPub(realpath(__DIR__) . '/test.copy.epub'); } public static function tearDownAfterClass() @@ -269,6 +269,27 @@ public function testSetCoverFile() ); } + public function testCancel() + { + $this->epub->setCoverFile(__DIR__ . '/test.jpg', 'image/jpeg'); + $this->epub->Title('fooooooooooooooooooooooooooooooooooooo'); + $this->epub->close(); + + clearstatcache($this->epub->getEPubLocation()); + $this->assertEquals(768780, filesize($this->epub->getEPubLocation())); + } + + public function testSave() + { + $this->epub->setCoverFile(__DIR__ . '/test.jpg', 'image/jpeg'); + $this->epub->Title('fooooooooooooooooooooooooooooooooooooo'); + $this->epub->save(); + + clearstatcache($this->epub->getEPubLocation()); + $this->assertNotEquals(768780, filesize($this->epub->getEPubLocation())); + } + + /*public function testCover(){ // read current cover $cover = $this->epub->Cover2(); From bc6f3a0775cbc0a91cf00d8b16ec349bf172b507 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 12:42:45 +0200 Subject: [PATCH 56/63] complete cover handling --- lib/EPub.php | 141 ++++------------------------------------------ test/epubTest.php | 64 ++++++++++++--------- 2 files changed, 47 insertions(+), 158 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index d1dd5a3..cb2bce8 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -100,7 +100,7 @@ protected function readManifest() 'id' => $node->attr('id'), 'mime' => $node->attr('opf:media-type'), 'exists' => (bool)$this->zip->FileExists($file), - 'path' => $file + 'path' => $file, ); } @@ -565,9 +565,14 @@ public function getCoverFile() /** * Removes the cover image if there's one + * + * If the actual image file was added by this library it will be removed. Otherwise only the + * reference to it is removed from the metadata, since the same image might be referenced + * by other parts of the epub file. */ public function clearCover() { + // do nothing if there's no cover currently $cover = $this->getCoverFile(); if ($cover === null) { return; @@ -585,10 +590,11 @@ public function clearCover() /** @var EPubDOMElement $node */ $node->delete(); } - - // FIXME we should remove the actual file from the archive if it was added by us, - //but the zip library used does not support deleting files, yet - + // remove the actual file if it was added by us + if($cover['id'] == 'php-epub-meta-cover') { + $this->zip->FileReplace($cover['path'], false); + $this->manifest = null; + } } /** @@ -675,96 +681,6 @@ protected function mkTocEntry(EPubDOMElement $node, EPubDOMXPath $toc_xpath) #endregion - /** - * 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->meta_xpath->query('//opf:metadata/opf:meta[@name="cover"]'); - foreach ($nodes as $node) { - /** @var EPubDOMElement $node */ - $node->delete(); - } - // remove previous manifest entries if they where made by us - $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="php-epub-meta-cover"]'); - foreach ($nodes as $node) { - /** @var EPubDOMElement $node */ - $node->delete(); - } - - if ($path) { - // add pointer - $parent = $this->meta_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->meta_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->meta_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->meta_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 - ); - } - public function combine($a, $b) { $isAbsolute = false; @@ -825,41 +741,6 @@ public function updateForKepub() } } - public function Cover2($path = false, $mime = false) - { - $hascover = true; - $item = $this->getCoverFile(); - if (is_null($item)) { - $hascover = false; - } else { - $mime = $item->attr('opf:media-type'); - $this->coverpath = $item->attr('opf:href'); - $this->coverpath = dirname('/' . $this->meta) . '/' . $this->coverpath; // image path is relative to meta file - $this->coverpath = ltrim($this->coverpath, '\\'); - $this->coverpath = ltrim($this->coverpath, '/'); - } - - // set cover - if ($path !== false) { - if (!$hascover) { - return; // TODO For now only update - } - - if ($path) { - $item->attr('opf:media-type', $mime); - - // remember path for save action - $this->imagetoadd = $path; - } - - $this->reparse(); - } - - if (!$hascover) { - return $this->no_cover(); - } - } - /** * A simple getter/setter for simple meta attributes * diff --git a/test/epubTest.php b/test/epubTest.php index 57c33d1..40cb590 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -281,42 +281,50 @@ public function testCancel() public function testSave() { + $epubfile = $this->epub->getEPubLocation(); + $this->epub->setCoverFile(__DIR__ . '/test.jpg', 'image/jpeg'); $this->epub->Title('fooooooooooooooooooooooooooooooooooooo'); $this->epub->save(); clearstatcache($this->epub->getEPubLocation()); - $this->assertNotEquals(768780, filesize($this->epub->getEPubLocation())); - } + $this->assertNotEquals(768780, filesize($epubfile)); + // reload the file + $this->epub = new EPub($epubfile); + $this->assertEquals('fooooooooooooooooooooooooooooooooooooo', $this->epub->Title()); - /*public function testCover(){ - // read current cover - $cover = $this->epub->Cover2(); - $this->assertEquals($cover['mime'],'image/png'); - $this->assertEquals($cover['found'],'OPS/images/cover.png'); - $this->assertEquals(strlen($cover['data']), 657911); + $this->assertEquals(array( + 'id' => 'php-epub-meta-cover', + 'mime' => 'image/jpeg', + 'exists' => true, + 'path' => 'OPS/php-epub-meta-cover.img', - // // delete cover // Don't work anymore - // $cover = $this->epub->Cover(''); - // $this->assertEquals($cover['mime'],'image/gif'); - // $this->assertEquals($cover['found'],false); - // $this->assertEquals(strlen($cover['data']), 42); + ), $this->epub->getCoverFile()); - // // set new cover (will return a not-found as it's not yet saved) - $cover = $this->epub->Cover2(realpath( dirname( __FILE__ ) ) . '/test.jpg','image/jpeg'); - // $this->assertEquals($cover['mime'],'image/jpeg'); - // $this->assertEquals($cover['found'],'OPS/php-epub-meta-cover.img'); - // $this->assertEquals(strlen($cover['data']), 0); + $this->assertEquals( + file_get_contents(__DIR__ . '/test.jpg'), + $this->epub->getFile('OPS/php-epub-meta-cover.img') + ); + + // test cover removing + $this->epub->clearCover(); + $this->assertNull($this->epub->getCoverFile()); + // our image should be gone + $this->assertEquals(array( + 'id' => '', + 'mime' => '', + 'exists' => false, + 'path' => 'OPS/php-epub-meta-cover.img', + ), $this->epub->getFileInfo('OPS/php-epub-meta-cover.img')); + // the original image should still be there, even though it's not registered as cover anymore + $this->assertEquals(array( + 'id' => 'book-cover', + 'mime' => 'image/png', + 'exists' => true, + 'path' => 'OPS/images/cover.png', + ), $this->epub->getFileInfo('OPS/images/cover.png')); + + } - // save - $this->epub->save(); - //$this->epub = new EPub(realpath( dirname( __FILE__ ) ) . '/test.copy.epub'); - - // read now changed cover - $cover = $this->epub->Cover2(); - $this->assertEquals($cover['mime'],'image/jpeg'); - $this->assertEquals($cover['found'],'OPS/images/cover.png'); - $this->assertEquals(strlen($cover['data']), filesize(realpath( dirname( __FILE__ ) ) . '/test.jpg')); - }*/ } From c390e05c569b15778e74d599b88e1a71efa4dbe8 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 12:50:18 +0200 Subject: [PATCH 57/63] some clean up --- lib/EPub.php | 44 +++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index cb2bce8..b63bc6e 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -12,23 +12,16 @@ class EPub { - /** @var string Location of the meta package within the epub */ protected $meta; /** @var EPubDOMDocument Parsed XML of the meta package */ public $meta_xml; /** @var EPubDOMXPath XPath access to the meta package */ protected $meta_xpath; - /** @var string The path to the epub file */ protected $file; - - /** @var \clsTbsZip */ + /** @var \clsTbsZip handles the ZIP operations on the epub file*/ protected $zip; - protected $coverpath = ''; - protected $namespaces; - protected $imagetoadd = ''; - /** @var null|array The manifest data, eg. which files are available */ protected $manifest = null; @@ -36,14 +29,13 @@ class EPub * Constructor * * @param string $file path to epub file to work on - * @param string $zipClass class to handle zip * @throws \Exception if metadata could not be loaded */ - public function __construct($file, $zipClass = 'clsTbsZip') + public function __construct($file) { // open file $this->file = $file; - $this->zip = new $zipClass(); + $this->zip = new \clsTbsZip(); if (!$this->zip->Open($this->file)) { throw new \Exception('Failed to read epub file'); } @@ -115,19 +107,6 @@ public function close() $this->zip->Close(); } - /** - * Remove iTunes files - */ - public function cleanITunesCrap() - { - if ($this->zip->FileExists('iTunesMetadata.plist')) { - $this->zip->FileReplace('iTunesMetadata.plist', false); - } - if ($this->zip->FileExists('iTunesArtwork')) { - $this->zip->FileReplace('iTunesArtwork', false); - } - } - /** * Writes back all changes to the epub */ @@ -156,7 +135,18 @@ public function download($file = null) } } - + /** + * Remove iTunes files + */ + public function cleanITunesCrap() + { + if ($this->zip->FileExists('iTunesMetadata.plist')) { + $this->zip->FileReplace('iTunesMetadata.plist', false); + } + if ($this->zip->FileExists('iTunesArtwork')) { + $this->zip->FileReplace('iTunesArtwork', false); + } + } #region Book Attribute Getter/Setters @@ -836,8 +826,8 @@ protected function no_cover() /** * Reparse the DOM tree * - * I had to rely on this because otherwise xpath failed to find the newly - * added nodes + * This is needed when new nodes are added to the DOM, otherwise XPath won't find those new + * nodes. */ protected function reparse() { From 51d86bfd1f0f139372f7ac156fa757df9da527e3 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 13:52:20 +0200 Subject: [PATCH 58/63] added general Date handling I decided against using DateTime objects since book dates are often not complete. Eg. 1597 is a valid publishing date, but DateTime would fill up this date with the current date. Eg. 1597-07-19 which makes not much sense. --- lib/EPub.php | 26 ++++++++++++++++---------- test/epubTest.php | 6 ++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index b63bc6e..9ed8631 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -20,7 +20,7 @@ class EPub protected $meta_xpath; /** @var string The path to the epub file */ protected $file; - /** @var \clsTbsZip handles the ZIP operations on the epub file*/ + /** @var \clsTbsZip handles the ZIP operations on the epub file */ protected $zip; /** @var null|array The manifest data, eg. which files are available */ protected $manifest = null; @@ -130,7 +130,7 @@ public function download($file = null) if ($file) { return $this->zip->Flush(TBSZIP_DOWNLOAD, $file); } else { - echo $this->zip->Flush(TBSZIP_STRING); + $this->zip->Flush(TBSZIP_STRING); return $this->zip->OutputSrc; // ugly but currently the only interface in the ZIP lib } } @@ -307,28 +307,34 @@ public function Uuid($uuid = null) * Set or get the book's creation date * * @param string|null $date Date eg: 2012-05-19T12:54:25Z - * @todo use DateTime class instead of string * @return string */ public function CreationDate($date = null) { - $res = $this->getset('dc:date', $date, 'opf:event', 'creation'); - - return $res; + return $this->Date('creation', $date); } /** * Set or get the book's modification date * * @param string|null $date Date eg: 2012-05-19T12:54:25Z - * @todo use DateTime class instead of string * @return string */ public function ModificationDate($date = null) { - $res = $this->getset('dc:date', $date, 'opf:event', 'modification'); + return $this->Date('modification', $date); + } - return $res; + /** + * Read or set a date + * + * @param string $type The type to set/read (modification|creation|conversion|publication|original-publication) + * @param string|null $date Date eg: 2012-05-19T12:54:25Z + * @return string + */ + public function Date($type, $date = null) + { + return $this->getset('dc:date', $date, 'opf:event', $type); } /** @@ -581,7 +587,7 @@ public function clearCover() $node->delete(); } // remove the actual file if it was added by us - if($cover['id'] == 'php-epub-meta-cover') { + if ($cover['id'] == 'php-epub-meta-cover') { $this->zip->FileReplace($cover['path'], false); $this->manifest = null; } diff --git a/test/epubTest.php b/test/epubTest.php index 40cb590..2df2dcb 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -231,6 +231,12 @@ public function testSubject() ); } + public function testDates() + { + $this->assertEquals('1597', $this->epub->Date('original-publication')); + $this->assertEquals('2008-09-18', $this->epub->Date('ops-publication')); + } + public function testGetCoverFile() { $expect = array( From 26aa53ec9ed9ec47ed9da6cae487e07162d1dcdf Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 14:14:17 +0200 Subject: [PATCH 59/63] use constants and general accessor for identifiers too --- lib/EPub.php | 93 +++++++++++------------------------------------ test/epubTest.php | 10 ++++- 2 files changed, 31 insertions(+), 72 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index 9ed8631..d81345b 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -12,6 +12,22 @@ class EPub { + #region Constants + const DATE_MODIFICATION = 'modification'; + const DATE_CREATION = 'creation'; + const DATE_CONVERSION = 'conversion'; + const DATE_PUB = 'publication'; + const DATE_PUB_ORIG = 'original-publication'; + + const IDENT_URI = 'URI'; + const IDENT_URN = 'URN'; + const IDENT_ISBN = 'ISBN'; + const IDENT_AMAZON = 'AMAZON'; + const IDENT_GOOGLE = 'GOOGLE'; + const IDENT_CALIBRE = 'CALIBRE'; + #endregion + + /** @var string Location of the meta package within the epub */ protected $meta; /** @var EPubDOMDocument Parsed XML of the meta package */ @@ -303,32 +319,10 @@ public function Uuid($uuid = null) return $res; } - /** - * Set or get the book's creation date - * - * @param string|null $date Date eg: 2012-05-19T12:54:25Z - * @return string - */ - public function CreationDate($date = null) - { - return $this->Date('creation', $date); - } - - /** - * Set or get the book's modification date - * - * @param string|null $date Date eg: 2012-05-19T12:54:25Z - * @return string - */ - public function ModificationDate($date = null) - { - return $this->Date('modification', $date); - } - /** * Read or set a date * - * @param string $type The type to set/read (modification|creation|conversion|publication|original-publication) + * @param string $type The type to set/read - use the DATE_* constants for typical dates * @param string|null $date Date eg: 2012-05-19T12:54:25Z * @return string */ @@ -338,62 +332,19 @@ public function Date($type, $date = null) } /** - * Set or get the book's URI + * Set or get the book's Identifier * - * @param string|null $uri URI + * @param string $type Type of identifier, use TYPE_* constants for typical + * @param string|null $ident Identifier * @return string */ - public function Uri($uri = null) + public function Identifier($type, $ident = null) { - $res = $this->getset('dc:identifier', $uri, 'opf:scheme', 'URI'); + $res = $this->getset('dc:identifier', $ident, 'opf:scheme', $type); return $res; } - /** - * Set or get the book's ISBN number - * - * @param string|null $isbn - * @return string - */ - public function ISBN($isbn = null) - { - return $this->getset('dc:identifier', $isbn, 'opf:scheme', 'ISBN'); - } - - /** - * Set or get the Google Books ID - * - * @param string|null $google - * @return string - */ - public function Google($google = null) - { - return $this->getset('dc:identifier', $google, 'opf:scheme', 'GOOGLE'); - } - - /** - * Set or get the Amazon ID of the book - * - * @param string|null $amazon - * @return string - */ - public function Amazon($amazon = null) - { - return $this->getset('dc:identifier', $amazon, 'opf:scheme', 'AMAZON'); - } - - /** - * Set or get the Calibre UUID of the book - * - * @param null|string $uuid - * @return string - */ - public function Calibre($uuid = null) - { - return $this->getset('dc:identifier', $uuid, 'opf:scheme', 'calibre'); - } - /** * Set or get the Series of the book * diff --git a/test/epubTest.php b/test/epubTest.php index 2df2dcb..cdb703e 100644 --- a/test/epubTest.php +++ b/test/epubTest.php @@ -2,6 +2,8 @@ namespace splitbrain\epubmeta\test; +use splitbrain\epubmeta\test\EPub; + class EPubTest extends \PHPUnit_Framework_TestCase { /** @var EPub */ @@ -233,10 +235,16 @@ public function testSubject() public function testDates() { - $this->assertEquals('1597', $this->epub->Date('original-publication')); + $this->assertEquals('1597', $this->epub->Date(EPub::DATE_PUB_ORIG)); $this->assertEquals('2008-09-18', $this->epub->Date('ops-publication')); } + public function testIdentifier() + { + $this->assertEquals('http://www.feedbooks.com/book/2936', $this->epub->Identifier(EPub::IDENT_URI)); + $this->assertEquals('urn:uuid:7d38d098-4234-11e1-97b6-001cc0a62c0b', $this->epub->Identifier(EPub::IDENT_URN)); + } + public function testGetCoverFile() { $expect = array( From 52b8059a1b9526533c6c587c898cea6b6887496f Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 21:25:12 +0200 Subject: [PATCH 60/63] removed unused methods, more cleanup --- lib/EPub.php | 82 +++++++++++++++++++--------------------------------- 1 file changed, 29 insertions(+), 53 deletions(-) diff --git a/lib/EPub.php b/lib/EPub.php index d81345b..6072138 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -27,7 +27,6 @@ class EPub const IDENT_CALIBRE = 'CALIBRE'; #endregion - /** @var string Location of the meta package within the epub */ protected $meta; /** @var EPubDOMDocument Parsed XML of the meta package */ @@ -164,6 +163,26 @@ public function cleanITunesCrap() } } + /** + * Makes sure the epub3 cover-image attribute is set + */ + public function updateForKepub() + { + $cover = $this->getCoverFile(); + if ($cover === null) { + return; + } + + $id = $cover['id']; + $nodes = $this->meta_xpath->query('//opf:manifest/opf:item[@id="' . $id . '"]'); + $node = $nodes->item(0); + if (!$node) { + return; + } + $node->attr('opf:properties', 'cover-image'); + } + + #region Book Attribute Getter/Setters /** @@ -573,6 +592,7 @@ public function setCoverFile($path, $mime) $node->attr('id', 'php-epub-meta-cover'); $node->attr('opf:href', 'php-epub-meta-cover.img'); $node->attr('opf:media-type', $mime); + $node->attr('opf:properties', 'cover-image'); $full = $this->getFullPath('php-epub-meta-cover.img'); @@ -628,64 +648,20 @@ protected function mkTocEntry(EPubDOMElement $node, EPubDOMXPath $toc_xpath) #endregion - public function combine($a, $b) - { - $isAbsolute = false; - if ($a[0] == '/') { - $isAbsolute = true; - } - - if ($b[0] == '/') { - throw new \InvalidArgumentException('Second path part must not start with /'); - } - - $splittedA = preg_split('#/#', $a); - $splittedB = preg_split('#/#', $b); - - $pathParts = array(); - $mergedPath = array_merge($splittedA, $splittedB); - - foreach ($mergedPath as $item) { - if ($item == null || $item == '' || $item == '.') { - continue; - } - - if ($item == '..') { - array_pop($pathParts); - continue; - } - - array_push($pathParts, $item); - } - - $path = implode('/', $pathParts); - if ($isAbsolute) { - return ('/' . $path); - } else { - return ($path); - } - } - - private function getFullPath($file, $context = null) + /** + * Resolves paths relative to the meta container location + * + * @param string $file relative path + * @return string full path within the zip + */ + private function getFullPath($file) { list($file) = explode('#', $file); // strip anchors - $path = dirname('/' . $this->meta) . '/' . $file; $path = ltrim($path, '\\'); $path = ltrim($path, '/'); - if (!empty($context)) { - $path = $this->combine(dirname($path), $context); - } - //error_log ("FullPath : $path ($file / $context)"); - return $path; - } - public function updateForKepub() - { - $item = $this->getCoverFile(); - if (!is_null($item)) { - $item->attr('opf:properties', 'cover-image'); - } + return $path; } /** From 145bd241b5474a28e235ab21ce3f7b0205bfe550 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 21:27:04 +0200 Subject: [PATCH 61/63] suppress IDEA warning --- lib/EPub.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/EPub.php b/lib/EPub.php index 6072138..fd40b68 100644 --- a/lib/EPub.php +++ b/lib/EPub.php @@ -146,6 +146,7 @@ public function download($file = null) return $this->zip->Flush(TBSZIP_DOWNLOAD, $file); } else { $this->zip->Flush(TBSZIP_STRING); + /** @noinspection PhpUndefinedFieldInspection */ return $this->zip->OutputSrc; // ugly but currently the only interface in the ZIP lib } } @@ -323,7 +324,7 @@ public function Description($description = null) * @param string|null $uuid Unique identifier * @return string * @throws \Exception - * @todo auto add unique identifer if needed + * @todo auto add unique identifer if needed? */ public function Uuid($uuid = null) { From d484ff7984fb3a78697f5b69033e03fc83cbfec1 Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 21:28:22 +0200 Subject: [PATCH 62/63] removed mercurial file --- .hgtags | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .hgtags diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 6d48e52..0000000 --- a/.hgtags +++ /dev/null @@ -1 +0,0 @@ -c5bdb12b19a22820b16b9f252b12e756f1ac8505 1.0.0 From a37ede72aefda8e1bf7d61e743761123335ff03e Mon Sep 17 00:00:00 2001 From: Andreas Gohr <andi@splitbrain.org> Date: Sat, 16 Jul 2016 21:35:36 +0200 Subject: [PATCH 63/63] adjust web manager --- index.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/index.php b/index.php index ea199e1..3744db9 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,8 @@ <?php // modify this to point to your book directory - $bookdir = '/home/andi/Dropbox/ebooks/'; +use splitbrain\epubmeta\EPub; + +$bookdir = '/home/andi/Dropbox/ebooks/'; // proxy google requests if (isset($_GET['api'])) { @@ -24,9 +26,9 @@ // return image data if (isset($_REQUEST['img']) && isset($epub)) { - $img = $epub->Cover(); + $img = $epub->getCoverFile(); header('Content-Type: ' . $img['mime']); - echo $img['data']; + echo $epub->getFile($img['path']); exit; } @@ -37,7 +39,7 @@ $epub->Language($_POST['language']); $epub->Publisher($_POST['publisher']); $epub->Copyright($_POST['copyright']); - $epub->ISBN($_POST['isbn']); + $epub->Identifier(EPub::IDENT_ISBN); $epub->Subjects($_POST['subjects']); $authors = array(); @@ -67,7 +69,7 @@ if ($cover) { $info = @getimagesize($cover); if (preg_match('/^image\/(gif|jpe?g|png)$/', $info['mime'])) { - $epub->Cover($cover, $info['meta']); + $epub->setCoverFile($cover, $info['mime']); } else { $error = 'Not a valid image file' . $cover; } @@ -90,7 +92,7 @@ $title = $epub->Title(); $new = to_file($author . '-' . $title); $new = $bookdir . $new . '.epub'; - $old = $epub->file(); + $old = $epub->getEPubLocation(); if (realpath($new) != realpath($old)) { if (!@rename($old, $new)) { $new = $old; //rename failed, stay here @@ -160,7 +162,7 @@ <tr> <th>Description<br /> <img src="?book=<?php echo htmlspecialchars($_REQUEST['book'])?>&img=1" id="cover" width="90" - class="<?php $c = $epub->Cover(); echo ($c['found']?'hasimg':'noimg')?>" /> + class="<?php $c = $epub->getCoverFile(); echo ($c?'hasimg':'noimg')?>" /> </th> <td><textarea name="description"><?php echo htmlspecialchars($epub->Description())?></textarea></td> </tr> @@ -182,7 +184,7 @@ class="<?php $c = $epub->Cover(); echo ($c['found']?'hasimg':'noimg')?>" /> </tr> <tr> <th>ISBN</th> - <td><p><input type="text" name="isbn" value="<?php echo htmlspecialchars($epub->ISBN())?>" /></p></td> + <td><p><input type="text" name="isbn" value="<?php echo htmlspecialchars($epub->Identifier(EPub::IDENT_ISBN))?>" /></p></td> </tr> <tr> <th>Cover Image</th>