diff --git a/inc/3rdparty/libraries/PHPePub/EPub.HtmlEntities.php b/inc/3rdparty/libraries/PHPePub/EPub.HtmlEntities.php
new file mode 100644
index 000000000..376b61336
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/EPub.HtmlEntities.php
@@ -0,0 +1,266 @@
+
\ No newline at end of file
diff --git a/inc/3rdparty/libraries/PHPePub/EPub.NCX.php b/inc/3rdparty/libraries/PHPePub/EPub.NCX.php
new file mode 100644
index 000000000..e5da05cdb
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/EPub.NCX.php
@@ -0,0 +1,782 @@
+
+ * @copyright 2009-2014 A. Grandt
+ * @license GNU LGPL, Attribution required for commercial implementations, requested for everything else.
+ * @version 3.20
+ */
+class Ncx {
+ const _VERSION = 3.20;
+
+ const MIMETYPE = "application/x-dtbncx+xml";
+
+ private $bookVersion = EPub::BOOK_VERSION_EPUB2;
+
+ private $navMap = NULL;
+ private $uid = NULL;
+ private $meta = array();
+ private $docTitle = NULL;
+ private $docAuthor = NULL;
+
+ private $currentLevel = NULL;
+ private $lastLevel = NULL;
+
+ private $languageCode = "en";
+ private $writingDirection = EPub::DIRECTION_LEFT_TO_RIGHT;
+
+ public $chapterList = array();
+ public $referencesTitle = "Guide";
+ public $referencesClass = "references";
+ public $referencesId = "references";
+ public $referencesList = array();
+ public $referencesName = array();
+ public $referencesOrder = NULL;
+
+ /**
+ * Class constructor.
+ *
+ * @param string $uid
+ * @param string $docTitle
+ * @param string $docAuthor
+ * @param string $languageCode
+ * @param string $writingDirection
+ */
+ function __construct($uid = NULL, $docTitle = NULL, $docAuthor = NULL, $languageCode = "en", $writingDirection = EPub::DIRECTION_LEFT_TO_RIGHT) {
+ $this->navMap = new NavMap($writingDirection);
+ $this->currentLevel = $this->navMap;
+ $this->setUid($uid);
+ $this->setDocTitle($docTitle);
+ $this->setDocAuthor($docAuthor);
+ $this->setLanguageCode($languageCode);
+ $this->setWritingDirection($writingDirection);
+ }
+
+ /**
+ * Class destructor
+ *
+ * @return void
+ */
+ function __destruct() {
+ unset($this->bookVersion, $this->navMap, $this->uid, $this->meta);
+ unset($this->docTitle, $this->docAuthor, $this->currentLevel, $this->lastLevel);
+ unset($this->languageCode, $this->writingDirection, $this->chapterList, $this->referencesTitle);
+ unset($this->referencesClass, $this->referencesId, $this->referencesList, $this->referencesName);
+ unset($this->referencesOrder);
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param string $bookVersion
+ */
+ function setVersion($bookVersion) {
+ $this->bookVersion = is_string($bookVersion) ? trim($bookVersion) : EPub::BOOK_VERSION_EPUB2;
+ }
+
+ /**
+ *
+ * @return bool TRUE if the book is set to type ePub 2
+ */
+ function isEPubVersion2() {
+ return $this->bookVersion === EPub::BOOK_VERSION_EPUB2;
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param string $uid
+ */
+ function setUid($uid) {
+ $this->uid = is_string($uid) ? trim($uid) : NULL;
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param string $docTitle
+ */
+ function setDocTitle($docTitle) {
+ $this->docTitle = is_string($docTitle) ? trim($docTitle) : NULL;
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param string $docAuthor
+ */
+ function setDocAuthor($docAuthor) {
+ $this->docAuthor = is_string($docAuthor) ? trim($docAuthor) : NULL;
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param string $languageCode
+ */
+ function setLanguageCode($languageCode) {
+ $this->languageCode = is_string($languageCode) ? trim($languageCode) : "en";
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param string $writingDirection
+ */
+ function setWritingDirection($writingDirection) {
+ $this->writingDirection = is_string($writingDirection) ? trim($writingDirection) : EPub::DIRECTION_LEFT_TO_RIGHT;
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param NavMap $navMap
+ */
+ function setNavMap($navMap) {
+ if ($navMap != NULL && is_object($navMap) && get_class($navMap) === "NavMap") {
+ $this->navMap = $navMap;
+ }
+ }
+
+ /**
+ * Add one chapter level.
+ *
+ * Subsequent chapters will be added to this level.
+ *
+ * @param string $navTitle
+ * @param string $navId
+ * @param string $navClass
+ * @param string $isNavHidden
+ * @param string $writingDirection
+ * @return NavPoint
+ */
+ function subLevel($navTitle = NULL, $navId = NULL, $navClass = NULL, $isNavHidden = FALSE, $writingDirection = NULL) {
+ $navPoint = FALSE;
+ if (isset($navTitle) && isset($navClass)) {
+ $navPoint = new NavPoint($navTitle, NULL, $navId, $navClass, $isNavHidden, $writingDirection);
+ $this->addNavPoint($navPoint);
+ }
+ if ($this->lastLevel !== NULL) {
+ $this->currentLevel = $this->lastLevel;
+ }
+ return $navPoint;
+ }
+
+ /**
+ * Step back one chapter level.
+ *
+ * Subsequent chapters will be added to this chapters parent level.
+ */
+ function backLevel() {
+ $this->lastLevel = $this->currentLevel;
+ $this->currentLevel = $this->currentLevel->getParent();
+ }
+
+ /**
+ * Step back to the root level.
+ *
+ * Subsequent chapters will be added to the rooot NavMap.
+ */
+ function rootLevel() {
+ $this->lastLevel = $this->currentLevel;
+ $this->currentLevel = $this->navMap;
+ }
+
+ /**
+ * Step back to the given level.
+ * Useful for returning to a previous level from deep within the structure.
+ * Values below 2 will have the same effect as rootLevel()
+ *
+ * @param int $newLevel
+ */
+ function setCurrentLevel($newLevel) {
+ if ($newLevel <= 1) {
+ $this->rootLevel();
+ } else {
+ while ($this->currentLevel->getLevel() > $newLevel) {
+ $this->backLevel();
+ }
+ }
+ }
+
+ /**
+ * Get current level count.
+ * The indentation of the current structure point.
+ *
+ * @return current level count;
+ */
+ function getCurrentLevel() {
+ return $this->currentLevel->getLevel();
+ }
+
+ /**
+ * Add child NavPoints to current level.
+ *
+ * @param NavPoint $navPoint
+ */
+ function addNavPoint($navPoint) {
+ $this->lastLevel = $this->currentLevel->addNavPoint($navPoint);
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @return NavMap
+ */
+ function getNavMap() {
+ return $this->navMap;
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @param string $name
+ * @param string $content
+ */
+ function addMetaEntry($name, $content) {
+ $name = is_string($name) ? trim($name) : NULL;
+ $content = is_string($content) ? trim($content) : NULL;
+
+ if ($name != NULL && $content != NULL) {
+ $this->meta[] = array($name => $content);
+ }
+ }
+
+ /**
+ *
+ * Enter description here ...
+ *
+ * @return string
+ */
+ function finalize() {
+ $nav = $this->navMap->finalize();
+
+ $ncx = "\n";
+ if ($this->isEPubVersion2()) {
+ $ncx .= "\n";
+ }
+ $ncx .= "
EPub version " . self::VERSION . " requires Zip.php at version " . self::REQ_ZIP_VERSION . " or higher.
You can obtain the latest version from http://www.phpclasses.org/browse/package/6110.html.
processCSSExternalReferences
for explanation. Default is EPub::EXTERNAL_REF_IGNORE.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root. NOT used if $externalReferences is set to EPub::EXTERNAL_REF_IGNORE.
+ *
+ * @return bool $success
+ */
+ function addCSSFile($fileName, $fileId, $fileData, $externalReferences = EPub::EXTERNAL_REF_IGNORE, $baseDir = "") {
+ if ($this->isFinalized || array_key_exists($fileName, $this->fileList)) {
+ return FALSE;
+ }
+ $fileName = Zip::getRelativePath($fileName);
+ $fileName = preg_replace('#^[/\.]+#i', "", $fileName);
+
+ if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
+ $cssDir = pathinfo($fileName);
+ $cssDir = preg_replace('#^[/\.]+#i', "", $cssDir["dirname"] . "/");
+ if (!empty($cssDir)) {
+ $cssDir = preg_replace('#[^/]+/#i', "../", $cssDir);
+ }
+
+ $this->processCSSExternalReferences($fileData, $externalReferences, $baseDir, $cssDir);
+ }
+
+ $this->addFile($fileName, "css_" . $fileId, $fileData, "text/css");
+
+ return TRUE;
+ }
+
+ /**
+ * Add a chapter to the book, as a chapter should not exceed 250kB, you can parse an array with multiple parts as $chapterData.
+ * These will still only show up as a single chapter in the book TOC.
+ *
+ * @param string $chapterName Name of the chapter, will be use din the TOC
+ * @param string $fileName Filename to use for the chapter, must be unique for the book.
+ * @param string $chapter Chapter text in XHTML or array $chapterData valid XHTML data for the chapter. File should NOT exceed 250kB.
+ * @param bool $autoSplit Should the chapter be split if it exceeds the default split size? Default=FALSE, only used if $chapterData is a string.
+ * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? See documentation for processChapterExternalReferences
for explanation. Default is EPub::EXTERNAL_REF_IGNORE.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root. NOT used if $externalReferences is set to EPub::EXTERNAL_REF_IGNORE.
+ * @return mixed $success FALSE if the addition failed, else the new NavPoint.
+ */
+ function addChapter($chapterName, $fileName, $chapterData = NULL, $autoSplit = FALSE, $externalReferences = EPub::EXTERNAL_REF_IGNORE, $baseDir = "") {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $fileName = Zip::getRelativePath($fileName);
+ $fileName = preg_replace('#^[/\.]+#i', "", $fileName);
+ $fileName = $this->sanitizeFileName($fileName);
+
+ $chapter = $chapterData;
+ if ($autoSplit && is_string($chapterData) && mb_strlen($chapterData) > $this->splitDefaultSize) {
+ $splitter = new EPubChapterSplitter();
+
+ $chapterArray = $splitter->splitChapter($chapterData);
+ if (count($chapterArray) > 1) {
+ $chapter = $chapterArray;
+ }
+ }
+
+ if (!empty($chapter) && is_string($chapter)) {
+ if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
+ $htmlDirInfo = pathinfo($fileName);
+ $htmlDir = preg_replace('#^[/\.]+#i', "", $htmlDirInfo["dirname"] . "/");
+ $this->processChapterExternalReferences($chapter, $externalReferences, $baseDir, $htmlDir);
+ }
+
+ if ($this->encodeHTML === TRUE) {
+ $chapter = $this->encodeHtml($chapter);
+ }
+
+ $this->chapterCount++;
+ $this->addFile($fileName, "chapter" . $this->chapterCount, $chapter, "application/xhtml+xml");
+ $this->opf->addItemRef("chapter" . $this->chapterCount);
+
+ $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $fileName, "chapter" . $this->chapterCount);
+ $this->ncx->addNavPoint($navPoint);
+ $this->ncx->chapterList[$chapterName] = $navPoint;
+ } else if (is_array($chapter)) {
+ $fileNameParts = pathinfo($fileName);
+ $extension = $fileNameParts['extension'];
+ $name = $fileNameParts['filename'];
+
+ $partCount = 0;
+ $this->chapterCount++;
+
+ $oneChapter = each($chapter);
+ while ($oneChapter) {
+ list($k, $v) = $oneChapter;
+ if ($this->encodeHTML === TRUE) {
+ $v = $this->encodeHtml($v);
+ }
+
+ if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
+ $this->processChapterExternalReferences($v, $externalReferences, $baseDir);
+ }
+ $partCount++;
+ $partName = $name . "_" . $partCount;
+ $this->addFile($partName . "." . $extension, $partName, $v, "application/xhtml+xml");
+ $this->opf->addItemRef($partName);
+
+ $oneChapter = each($chapter);
+ }
+ $partName = $name . "_1." . $extension;
+ $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $partName, $partName);
+ $this->ncx->addNavPoint($navPoint);
+
+ $this->ncx->chapterList[$chapterName] = $navPoint;
+ } else if (!isset($chapterData) && strpos($fileName, "#") > 0) {
+ $this->chapterCount++;
+ //$this->opf->addItemRef("chapter" . $this->chapterCount);
+
+ $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $fileName, "chapter" . $this->chapterCount);
+ $this->ncx->addNavPoint($navPoint);
+ $this->ncx->chapterList[$chapterName] = $navPoint;
+ } else if (!isset($chapterData) && $fileName=="TOC.xhtml") {
+ $this->chapterCount++;
+ $this->opf->addItemRef("toc");
+
+ $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $fileName, "chapter" . $this->chapterCount);
+ $this->ncx->addNavPoint($navPoint);
+ $this->ncx->chapterList[$chapterName] = $navPoint;
+ }
+ return $navPoint;
+ }
+
+ /**
+ * Add one chapter level.
+ *
+ * Subsequent chapters will be added to this level.
+ *
+ * @param string $navTitle
+ * @param string $navId
+ * @param string $navClass
+ * @param int $isNavHidden
+ * @param string $writingDirection
+ * @return NavPoint The new NavPoint for that level.
+ */
+ function subLevel($navTitle = NULL, $navId = NULL, $navClass = NULL, $isNavHidden = FALSE, $writingDirection = NULL) {
+ return $this->ncx->subLevel($this->decodeHtmlEntities($navTitle), $navId, $navClass, $isNavHidden, $writingDirection);
+ }
+
+ /**
+ * Step back one chapter level.
+ *
+ * Subsequent chapters will be added to this chapters parent level.
+ */
+ function backLevel() {
+ $this->ncx->backLevel();
+ }
+
+ /**
+ * Step back to the root level.
+ *
+ * Subsequent chapters will be added to the rooot NavMap.
+ */
+ function rootLevel() {
+ $this->ncx->rootLevel();
+ }
+
+ /**
+ * Step back to the given level.
+ * Useful for returning to a previous level from deep within the structure.
+ * Values below 2 will have the same effect as rootLevel()
+ *
+ * @param int $newLevel
+ */
+ function setCurrentLevel($newLevel) {
+ $this->ncx->setCurrentLevel($newLevel);
+ }
+
+ /**
+ * Get current level count.
+ * The indentation of the current structure point.
+ *
+ * @return current level count;
+ */
+ function getCurrentLevel() {
+ return $this->ncx->getCurrentLevel();
+ }
+
+ /**
+ * Wrap ChapterContent with Head and Footer
+ *
+ * @param $content
+ * @return string $content
+ */
+ private function wrapChapter($content) {
+ return $this->htmlContentHeader . "\n" . $content . "\n" . $this->htmlContentFooter;
+ }
+
+ /**
+ * Reference pages is usually one or two pages for items such as Table of Contents, reference lists, Author notes or Acknowledgements.
+ * These do not show up in the regular navigation list.
+ *
+ * As they are supposed to be short.
+ *
+ * @param string $pageName Name of the chapter, will be use din the TOC
+ * @param string $fileName Filename to use for the chapter, must be unique for the book.
+ * @param string $pageData Page content in XHTML. File should NOT exceed 250kB.
+ * @param string $reference Reference key
+ * @param int $externalReferences How to handle external references. See documentation for processChapterExternalReferences
for explanation. Default is EPub::EXTERNAL_REF_IGNORE.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root. NOT used if $externalReferences is set to EPub::EXTERNAL_REF_IGNORE.
+ * @return bool $success
+ */
+ function addReferencePage($pageName, $fileName, $pageData, $reference, $externalReferences = EPub::EXTERNAL_REF_IGNORE, $baseDir = "") {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $fileName = Zip::getRelativePath($fileName);
+ $fileName = preg_replace('#^[/\.]+#i', "", $fileName);
+
+
+ if (!empty($pageData) && is_string($pageData)) {
+ if ($this->encodeHTML === TRUE) {
+ $pageData = $this->encodeHtml($pageData);
+ }
+
+ $this->wrapChapter($pageData);
+
+ if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
+ $htmlDirInfo = pathinfo($fileName);
+ $htmlDir = preg_replace('#^[/\.]+#i', "", $htmlDirInfo["dirname"] . "/");
+ $this->processChapterExternalReferences($pageData, $externalReferences, $baseDir, $htmlDir);
+ }
+
+ $this->addFile($fileName, "ref_" . $reference, $pageData, "application/xhtml+xml");
+
+ if ($reference !== Reference::TABLE_OF_CONTENTS || !isset($this->ncx->referencesList[$reference])) {
+ $this->opf->addItemRef("ref_" . $reference, FALSE);
+ $this->opf->addReference($reference, $pageName, $fileName);
+
+ $this->ncx->referencesList[$reference] = $fileName;
+ $this->ncx->referencesName[$reference] = $pageName;
+ }
+ return TRUE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * Add custom metadata to the book.
+ *
+ * It is up to the builder to make sure there are no collisions. Metadata are just key value pairs.
+ *
+ * @param string $name
+ * @param string $content
+ */
+ function addCustomMetadata($name, $content) {
+ $this->opf->addMeta($name, $content);
+ }
+
+ /**
+ * Add DublinCore metadata to the book
+ *
+ * Use the DublinCore constants included in EPub, ie DublinCore::DATE
+ *
+ * @param string $dublinCore name
+ * @param string $value
+ */
+ function addDublinCoreMetadata($dublinCoreConstant, $value) {
+ if ($this->isFinalized) {
+ return;
+ }
+
+ $this->opf->addDCMeta($dublinCoreConstant, $this->decodeHtmlEntities($value));
+ }
+
+ /**
+ * Add a cover image to the book.
+ * If the $imageData is not set, the function assumes the $fileName is the path to the image file.
+ *
+ * The styling and structure of the generated XHTML is heavily inspired by the XHTML generated by Calibre.
+ *
+ * @param string $fileName Filename to use for the image, must be unique for the book.
+ * @param string $imageData Binary image data
+ * @param string $mimetype Image mimetype, such as "image/jpeg" or "image/png".
+ * @return bool $success
+ */
+ function setCoverImage($fileName, $imageData = NULL, $mimetype = NULL,$bookTitle) {
+ if ($this->isFinalized || $this->isCoverImageSet || array_key_exists("CoverPage.html", $this->fileList)) {
+ return FALSE;
+ }
+
+ if ($imageData == NULL) {
+ // assume $fileName is the valid file path.
+ if (!file_exists($fileName)) {
+ // Attempt to locate the file using the doc root.
+ $rp = realpath($this->docRoot . "/" . $fileName);
+
+ if ($rp !== FALSE) {
+ // only assign the docroot path if it actually exists there.
+ $fileName = $rp;
+ }
+ }
+ $image = $this->getImage($fileName);
+ $imageData = $image['image'];
+ $mimetype = $image['mime'];
+ $fileName = preg_replace("#\.[^\.]+$#", "." . $image['ext'], $fileName);
+ }
+
+
+ $path = pathinfo($fileName);
+ $imgPath = "images/" . $path["basename"];
+
+ if (empty($mimetype) && file_exists($fileName)) {
+ list($width, $height, $type, $attr) = getimagesize($fileName);
+ $mimetype = image_type_to_mime_type($type);
+ }
+ if (empty($mimetype)) {
+ $ext = strtolower($path['extension']);
+ if ($ext == "jpg") {
+ $ext = "jpeg";
+ }
+ $mimetype = "image/" . $ext;
+ }
+
+ $coverPage = "";
+
+ if ($this->isEPubVersion2()) {
+ $coverPage = "\n"
+ . "\n"
+ . "\n"
+ . "\t\n"
+ . "\t\t\n"
+ . "\t\t<img src="../images/image.png"/>
+ *
+ * $externalReferences determines how the function will handle external references.
+ *
+ * @param mixed &$doc (referenced)
+ * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
+ *
+ * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
+ */
+ protected function processChapterExternalReferences(&$doc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "") {
+ if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
+ return FALSE;
+ }
+
+ $backPath = preg_replace('#[^/]+/#i', "../", $htmlDir);
+ $isDocAString = is_string($doc);
+ $xmlDoc = NULL;
+
+ if ($isDocAString) {
+ $xmlDoc = new DOMDocument();
+ @$xmlDoc->loadHTML($doc);
+ } else {
+ $xmlDoc = $doc;
+ }
+
+ $this->processChapterStyles($xmlDoc, $externalReferences, $baseDir, $htmlDir);
+ $this->processChapterLinks($xmlDoc, $externalReferences, $baseDir, $htmlDir, $backPath);
+ $this->processChapterImages($xmlDoc, $externalReferences, $baseDir, $htmlDir, $backPath);
+ $this->processChapterSources($xmlDoc, $externalReferences, $baseDir, $htmlDir, $backPath);
+
+ if ($isDocAString) {
+ //$html = $xmlDoc->saveXML();
+
+ $htmlNode = $xmlDoc->getElementsByTagName("html");
+ $headNode = $xmlDoc->getElementsByTagName("head");
+ $bodyNode = $xmlDoc->getElementsByTagName("body");
+
+ $htmlNS = "";
+ for ($index = 0; $index < $htmlNode->item(0)->attributes->length; $index++) {
+ $nodeName = $htmlNode->item(0)->attributes->item($index)->nodeName;
+ $nodeValue = $htmlNode->item(0)->attributes->item($index)->nodeValue;
+
+ if ($nodeName != "xmlns") {
+ $htmlNS .= " $nodeName=\"$nodeValue\"";
+ }
+ }
+
+ $xml = new DOMDocument('1.0', "utf-8");
+ $xml->lookupPrefix("http://www.w3.org/1999/xhtml");
+ $xml->preserveWhiteSpace = FALSE;
+ $xml->formatOutput = TRUE;
+
+ $xml2Doc = new DOMDocument('1.0', "utf-8");
+ $xml2Doc->lookupPrefix("http://www.w3.org/1999/xhtml");
+ $xml2Doc->loadXML("\n\n\n\n");
+ $html = $xml2Doc->getElementsByTagName("html")->item(0);
+ $html->appendChild($xml2Doc->importNode($headNode->item(0), TRUE));
+ $html->appendChild($xml2Doc->importNode($bodyNode->item(0), TRUE));
+
+ // force pretty printing and correct formatting, should not be needed, but it is.
+ $xml->loadXML($xml2Doc->saveXML());
+ $doc = $xml->saveXML();
+
+ if (!$this->isEPubVersion2()) {
+ $doc = preg_replace('#^\s*\s*#im', '', $doc);
+ }
+ }
+ return TRUE;
+ }
+
+ /**
+ * Process images referenced from an CSS file to the book.
+ *
+ * $externalReferences determins how the function will handle external references.
+ *
+ * @param string &$cssFile (referenced)
+ * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $cssDir The of the CSS file's directory from the root of the archive.
+ *
+ * @return bool FALSE if unsuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
+ */
+ protected function processCSSExternalReferences(&$cssFile, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $cssDir = "") {
+ if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
+ return FALSE;
+ }
+
+ $backPath = preg_replace('#[^/]+/#i', "../", $cssDir);
+ $imgs = null;
+ preg_match_all('#url\s*\([\'\"\s]*(.+?)[\'\"\s]*\)#im', $cssFile, $imgs, PREG_SET_ORDER);
+
+ $itemCount = count($imgs);
+ for ($idx = 0; $idx < $itemCount; $idx++) {
+ $img = $imgs[$idx];
+ if ($externalReferences === EPub::EXTERNAL_REF_REMOVE_IMAGES || $externalReferences === EPub::EXTERNAL_REF_REPLACE_IMAGES) {
+ $cssFile = str_replace($img[0], "", $cssFile);
+ } else {
+ $source = $img[1];
+
+ $pathData = pathinfo($source);
+ $internalSrc = $pathData['basename'];
+ $internalPath = "";
+ $isSourceExternal = FALSE;
+
+ if ($this->resolveImage($source, $internalPath, $internalSrc, $isSourceExternal, $baseDir, $cssDir, $backPath)) {
+ $cssFile = str_replace($img[0], "url('" . $backPath . $internalPath . "')", $cssFile);
+ } else if ($isSourceExternal) {
+ $cssFile = str_replace($img[0], "", $cssFile); // External image is missing
+ } // else do nothing, if the image is local, and missing, assume it's been generated.
+ }
+ }
+ return TRUE;
+ }
+
+ /**
+ * Process style tags in a DOMDocument. Styles will be passed as CSS files and reinserted into the document.
+ *
+ * @param DOMDocument &$xmlDoc (referenced)
+ * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
+ *
+ * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
+ */
+ protected function processChapterStyles(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "") {
+ if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
+ return FALSE;
+ }
+ // process inlined CSS styles in style tags.
+ $styles = $xmlDoc->getElementsByTagName("style");
+ $styleCount = $styles->length;
+ for ($styleIdx = 0; $styleIdx < $styleCount; $styleIdx++) {
+ $style = $styles->item($styleIdx);
+
+ $styleData = preg_replace('#[/\*\s]*\<\!\[CDATA\[[\s\*/]*#im', "", $style->nodeValue);
+ $styleData = preg_replace('#[/\*\s]*\]\]\>[\s\*/]*#im', "", $styleData);
+
+ $this->processCSSExternalReferences($styleData, $externalReferences, $baseDir, $htmlDir);
+ $style->nodeValue = "\n" . trim($styleData) . "\n";
+ }
+ return TRUE;
+ }
+
+ /**
+ * Process link tags in a DOMDocument. Linked files will be loaded into the archive, and the link src will be rewritten to point to that location.
+ * Link types text/css will be passed as CSS files.
+ *
+ * @param DOMDocument &$xmlDoc (referenced)
+ * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
+ * @param string $backPath The path to get back to the root of the archive from $htmlDir.
+ *
+ * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
+ */
+ protected function processChapterLinks(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "", $backPath = "") {
+ if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
+ return FALSE;
+ }
+ // process link tags.
+ $links = $xmlDoc->getElementsByTagName("link");
+ $linkCount = $links->length;
+ for ($linkIdx = 0; $linkIdx < $linkCount; $linkIdx++) {
+ $link = $links->item($linkIdx);
+ $source = $link->attributes->getNamedItem("href")->nodeValue;
+ $sourceData = NULL;
+
+ $pathData = pathinfo($source);
+ $internalSrc = $pathData['basename'];
+
+ if (preg_match('#^(http|ftp)s?://#i', $source) == 1) {
+ $urlinfo = parse_url($source);
+
+ if (strpos($urlinfo['path'], $baseDir."/") !== FALSE) {
+ $internalSrc = substr($urlinfo['path'], strpos($urlinfo['path'], $baseDir."/") + strlen($baseDir) + 1);
+ }
+
+ @$sourceData = getFileContents($source);
+ } else if (strpos($source, "/") === 0) {
+ @$sourceData = file_get_contents($this->docRoot . $source);
+ } else {
+ @$sourceData = file_get_contents($this->docRoot . $baseDir . "/" . $source);
+ }
+
+ if (!empty($sourceData)) {
+ if (!array_key_exists($internalSrc, $this->fileList)) {
+ $mime = $link->attributes->getNamedItem("type")->nodeValue;
+ if (empty($mime)) {
+ $mime = "text/plain";
+ }
+ if ($mime == "text/css") {
+ $this->processCSSExternalReferences($sourceData, $externalReferences, $baseDir, $htmlDir);
+ $this->addCSSFile($internalSrc, $internalSrc, $sourceData, EPub::EXTERNAL_REF_IGNORE, $baseDir);
+ $link->setAttribute("href", $backPath . $internalSrc);
+ } else {
+ $this->addFile($internalSrc, $internalSrc, $sourceData, $mime);
+ }
+ $this->fileList[$internalSrc] = $source;
+ } else {
+ $link->setAttribute("href", $backPath . $internalSrc);
+ }
+ } // else do nothing, if the link is local, and missing, assume it's been generated.
+ }
+ return TRUE;
+ }
+
+ /**
+ * Process img tags in a DOMDocument.
+ * $externalReferences will determine what will happen to these images, and the img src will be rewritten accordingly.
+ *
+ * @param DOMDocument &$xmlDoc (referenced)
+ * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
+ * @param string $backPath The path to get back to the root of the archive from $htmlDir.
+ *
+ * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
+ */
+ protected function processChapterImages(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "", $backPath = "") {
+ if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
+ return FALSE;
+ }
+ // process img tags.
+ $postProcDomElememts = array();
+ $images = $xmlDoc->getElementsByTagName("img");
+ $itemCount = $images->length;
+
+ for ($idx = 0; $idx < $itemCount; $idx++) {
+ $img = $images->item($idx);
+
+ if ($externalReferences === EPub::EXTERNAL_REF_REMOVE_IMAGES) {
+ $postProcDomElememts[] = $img;
+ } else if ($externalReferences === EPub::EXTERNAL_REF_REPLACE_IMAGES) {
+ $altNode = $img->attributes->getNamedItem("alt");
+ $alt = "image";
+ if ($altNode !== NULL && strlen($altNode->nodeValue) > 0) {
+ $alt = $altNode->nodeValue;
+ }
+ $postProcDomElememts[] = array($img, $this->createDomFragment($xmlDoc, "[" . $alt . "]"));
+ } else {
+ $source = $img->attributes->getNamedItem("src")->nodeValue;
+
+ $parsedSource = parse_url($source);
+ $internalSrc = $this->sanitizeFileName(urldecode(pathinfo($parsedSource['path'], PATHINFO_BASENAME)));
+ $internalPath = "";
+ $isSourceExternal = FALSE;
+
+ if ($this->resolveImage($source, $internalPath, $internalSrc, $isSourceExternal, $baseDir, $htmlDir, $backPath)) {
+ $img->setAttribute("src", $backPath . $internalPath);
+ } else if ($isSourceExternal) {
+ $postProcDomElememts[] = $img; // External image is missing
+ } // else do nothing, if the image is local, and missing, assume it's been generated.
+ }
+ }
+
+ foreach ($postProcDomElememts as $target) {
+ if (is_array($target)) {
+ $target[0]->parentNode->replaceChild($target[1], $target[0]);
+ } else {
+ $target->parentNode->removeChild($target);
+ }
+ }
+ return TRUE;
+ }
+
+ /**
+ * Process source tags in a DOMDocument.
+ * $externalReferences will determine what will happen to these images, and the img src will be rewritten accordingly.
+ *
+ * @param DOMDocument &$xmlDoc (referenced)
+ * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
+ * @param string $backPath The path to get back to the root of the archive from $htmlDir.
+ *
+ * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
+ */
+ protected function processChapterSources(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "", $backPath = "") {
+ if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
+ return FALSE;
+ }
+
+ if ($this->bookVersion !== EPub::BOOK_VERSION_EPUB3) {
+ // ePub 2 does not support multimedia formats, and they must be removed.
+ $externalReferences = EPub::EXTERNAL_REF_REMOVE_IMAGES;
+ }
+
+ $postProcDomElememts = array();
+ $images = $xmlDoc->getElementsByTagName("source");
+ $itemCount = $images->length;
+ for ($idx = 0; $idx < $itemCount; $idx++) {
+ $img = $images->item($idx);
+ if ($externalReferences === EPub::EXTERNAL_REF_REMOVE_IMAGES) {
+ $postProcDomElememts[] = $img;
+ } else if ($externalReferences === EPub::EXTERNAL_REF_REPLACE_IMAGES) {
+ $altNode = $img->attributes->getNamedItem("alt");
+ $alt = "image";
+ if ($altNode !== NULL && strlen($altNode->nodeValue) > 0) {
+ $alt = $altNode->nodeValue;
+ }
+ $postProcDomElememts[] = array($img, $this->createDomFragment($xmlDoc, "[" . $alt . "]"));
+ } else {
+ $source = $img->attributes->getNamedItem("src")->nodeValue;
+
+ $parsedSource = parse_url($source);
+ $internalSrc = $this->sanitizeFileName(urldecode(pathinfo($parsedSource['path'], PATHINFO_BASENAME)));
+ $internalPath = "";
+ $isSourceExternal = FALSE;
+
+ if ($this->resolveMedia($source, $internalPath, $internalSrc, $isSourceExternal, $baseDir, $htmlDir, $backPath)) {
+ $img->setAttribute("src", $backPath . $internalPath);
+ } else if ($isSourceExternal) {
+ $postProcDomElememts[] = $img; // External image is missing
+ } // else do nothing, if the image is local, and missing, assume it's been generated.
+ }
+ }
+ }
+
+ /**
+ * Resolve an image src and determine it's target location and add it to the book.
+ *
+ * @param string $source Image Source link.
+ * @param string &$internalPath (referenced) Return value, will be set to the target path and name in the book.
+ * @param string &$internalSrc (referenced) Return value, will be set to the target name in the book.
+ * @param string &$isSourceExternal (referenced) Return value, will be set to TRUE if the image originated from a full URL.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
+ * @param string $backPath The path to get back to the root of the archive from $htmlDir.
+ */
+ protected function resolveImage($source, &$internalPath, &$internalSrc, &$isSourceExternal, $baseDir = "", $htmlDir = "", $backPath = "") {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $imageData = NULL;
+
+ if (preg_match('#^(http|ftp)s?://#i', $source) == 1) {
+ $urlinfo = parse_url($source);
+ $urlPath = pathinfo($urlinfo['path']);
+
+ if (strpos($urlinfo['path'], $baseDir."/") !== FALSE) {
+ $internalSrc = $this->sanitizeFileName(urldecode(substr($urlinfo['path'], strpos($urlinfo['path'], $baseDir."/") + strlen($baseDir) + 1)));
+ }
+ $internalPath = $urlinfo["scheme"] . "/" . $urlinfo["host"] . "/" . pathinfo($urlinfo["path"], PATHINFO_DIRNAME);
+ $isSourceExternal = TRUE;
+ $imageData = $this->getImage($source);
+ } else if (strpos($source, "/") === 0) {
+ $internalPath = pathinfo($source, PATHINFO_DIRNAME);
+
+ $path = $source;
+ if (!file_exists($path)) {
+ $path = $this->docRoot . $path;
+ }
+
+ $imageData = $this->getImage($path);
+ } else {
+ $internalPath = $htmlDir . "/" . preg_replace('#^[/\.]+#', '', pathinfo($source, PATHINFO_DIRNAME));
+
+ $path = $baseDir . "/" . $source;
+ if (!file_exists($path)) {
+ $path = $this->docRoot . $path;
+ }
+
+ $imageData = $this->getImage($path);
+ }
+ if ($imageData !== FALSE) {
+ $iSrcInfo = pathinfo($internalSrc);
+ if (!empty($imageData['ext']) && $imageData['ext'] != $iSrcInfo['extension']) {
+ $internalSrc = $iSrcInfo['filename'] . "." . $imageData['ext'];
+ }
+ $internalPath = Zip::getRelativePath("images/" . $internalPath . "/" . $internalSrc);
+ if (!array_key_exists($internalPath, $this->fileList)) {
+ $this->addFile($internalPath, "i_" . $internalSrc, $imageData['image'], $imageData['mime']);
+ $this->fileList[$internalPath] = $source;
+ }
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Resolve a media src and determine it's target location and add it to the book.
+ *
+ * @param string $source Source link.
+ * @param string $internalPath (referenced) Return value, will be set to the target path and name in the book.
+ * @param string $internalSrc (referenced) Return value, will be set to the target name in the book.
+ * @param string $isSourceExternal (referenced) Return value, will be set to TRUE if the image originated from a full URL.
+ * @param string $baseDir Default is "", meaning it is pointing to the document root.
+ * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
+ * @param string $backPath The path to get back to the root of the archive from $htmlDir.
+ */
+ protected function resolveMedia($source, &$internalPath, &$internalSrc, &$isSourceExternal, $baseDir = "", $htmlDir = "", $backPath = "") {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $mediaPath = NULL;
+ $tmpFile;
+
+ if (preg_match('#^(http|ftp)s?://#i', $source) == 1) {
+ $urlinfo = parse_url($source);
+
+ if (strpos($urlinfo['path'], $baseDir."/") !== FALSE) {
+ $internalSrc = substr($urlinfo['path'], strpos($urlinfo['path'], $baseDir."/") + strlen($baseDir) + 1);
+ }
+ $internalPath = $urlinfo["scheme"] . "/" . $urlinfo["host"] . "/" . pathinfo($urlinfo["path"], PATHINFO_DIRNAME);
+ $isSourceExternal = TRUE;
+ $mediaPath = $this->getFileContents($source, true);
+ $tmpFile = $mediaPath;
+ } else if (strpos($source, "/") === 0) {
+ $internalPath = pathinfo($source, PATHINFO_DIRNAME);
+
+ $mediaPath = $source;
+ if (!file_exists($mediaPath)) {
+ $mediaPath = $this->docRoot . $mediaPath;
+ }
+ } else {
+ $internalPath = $htmlDir . "/" . preg_replace('#^[/\.]+#', '', pathinfo($source, PATHINFO_DIRNAME));
+
+ $mediaPath = $baseDir . "/" . $source;
+ if (!file_exists($mediaPath)) {
+ $mediaPath = $this->docRoot . $mediaPath;
+ }
+ }
+
+ if ($mediaPath !== FALSE) {
+ $mime = $this->getMime($source);
+ $internalPath = Zip::getRelativePath("media/" . $internalPath . "/" . $internalSrc);
+
+ if (!array_key_exists($internalPath, $this->fileList) &&
+ $this->addLargeFile($internalPath, "m_" . $internalSrc, $mediaPath, $mime)) {
+ $this->fileList[$internalPath] = $source;
+ }
+ if (isset($tmpFile)) {
+ unlink($tmpFile);
+ }
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Get Book Chapter count.
+ *
+ * @access public
+ * @return number of chapters
+ */
+ function getChapterCount() {
+ return $this->chapterCount;
+ }
+
+ /**
+ * Book title, mandatory.
+ *
+ * Used for the dc:title metadata parameter in the OPF file as well as the DocTitle attribute in the NCX file.
+ *
+ * @param string $title
+ * @access public
+ * @return bool $success
+ */
+ function setTitle($title) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->title = $title;
+ return TRUE;
+ }
+
+ /**
+ * Get Book title.
+ *
+ * @access public
+ * @return $title
+ */
+ function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Book language, mandatory
+ *
+ * Use the RFC3066 Language codes, such as "en", "da", "fr" etc.
+ * Defaults to "en".
+ *
+ * Used for the dc:language metadata parameter in the OPF file.
+ *
+ * @param string $language
+ * @access public
+ * @return bool $success
+ */
+ function setLanguage($language) {
+ if ($this->isFinalized || mb_strlen($language) != 2) {
+ return FALSE;
+ }
+ $this->language = $language;
+ return TRUE;
+ }
+
+ /**
+ * Get Book language.
+ *
+ * @access public
+ * @return $language
+ */
+ function getLanguage() {
+ return $this->language;
+ }
+
+ /**
+ * Unique book identifier, mandatory.
+ * Use the URI, or ISBN if available.
+ *
+ * An unambiguous reference to the resource within a given context.
+ *
+ * Recommended best practice is to identify the resource by means of a
+ * string conforming to a formal identification system.
+ *
+ * Used for the dc:identifier metadata parameter in the OPF file, as well
+ * as dtb:uid in the NCX file.
+ *
+ * Identifier type should only be:
+ * EPub::IDENTIFIER_URI
+ * EPub::IDENTIFIER_ISBN
+ * EPub::IDENTIFIER_UUID
+ *
+ * @param string $identifier
+ * @param string $identifierType
+ * @access public
+ * @return bool $success
+ */
+ function setIdentifier($identifier, $identifierType) {
+ if ($this->isFinalized || ($identifierType !== EPub::IDENTIFIER_URI && $identifierType !== EPub::IDENTIFIER_ISBN && $identifierType !== EPub::IDENTIFIER_UUID)) {
+ return FALSE;
+ }
+ $this->identifier = $identifier;
+ $this->identifierType = $identifierType;
+ return TRUE;
+ }
+
+ /**
+ * Get Book identifier.
+ *
+ * @access public
+ * @return $identifier
+ */
+ function getIdentifier() {
+ return $this->identifier;
+ }
+
+ /**
+ * Get Book identifierType.
+ *
+ * @access public
+ * @return $identifierType
+ */
+ function getIdentifierType() {
+ return $this->identifierType;
+ }
+
+ /**
+ * Book description, optional.
+ *
+ * An account of the resource.
+ *
+ * Description may include but is not limited to: an abstract, a table of
+ * contents, a graphical representation, or a free-text account of the
+ * resource.
+ *
+ * Used for the dc:source metadata parameter in the OPF file
+ *
+ * @param string $description
+ * @access public
+ * @return bool $success
+ */
+ function setDescription($description) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->description = $description;
+ return TRUE;
+ }
+
+ /**
+ * Get Book description.
+ *
+ * @access public
+ * @return $description
+ */
+ function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * Book author or creator, optional.
+ * The $authorSortKey is basically how the name is to be sorted, usually
+ * it's "Lastname, First names" where the $author is the straight
+ * "Firstnames Lastname"
+ *
+ * An entity primarily responsible for making the resource.
+ *
+ * Examples of a Creator include a person, an organization, or a service.
+ * Typically, the name of a Creator should be used to indicate the entity.
+ *
+ * Used for the dc:creator metadata parameter in the OPF file and the
+ * docAuthor attribure in the NCX file.
+ * The sort key is used for the opf:file-as attribute in dc:creator.
+ *
+ * @param string $author
+ * @param string $authorSortKey
+ * @access public
+ * @return bool $success
+ */
+ function setAuthor($author, $authorSortKey) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->author = $author;
+ $this->authorSortKey = $authorSortKey;
+ return TRUE;
+ }
+
+ /**
+ * Get Book author.
+ *
+ * @access public
+ * @return $author
+ */
+ function getAuthor() {
+ return $this->author;
+ }
+
+ /**
+ * Publisher Information, optional.
+ *
+ * An entity responsible for making the resource available.
+ *
+ * Examples of a Publisher include a person, an organization, or a service.
+ * Typically, the name of a Publisher should be used to indicate the entity.
+ *
+ * Used for the dc:publisher and dc:relation metadata parameters in the OPF file.
+ *
+ * @param string $publisherName
+ * @param string $publisherURL
+ * @access public
+ * @return bool $success
+ */
+ function setPublisher($publisherName, $publisherURL) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->publisherName = $publisherName;
+ $this->publisherURL = $publisherURL;
+ return TRUE;
+ }
+
+ /**
+ * Get Book publisherName.
+ *
+ * @access public
+ * @return $publisherName
+ */
+ function getPublisherName() {
+ return $this->publisherName;
+ }
+
+ /**
+ * Get Book publisherURL.
+ *
+ * @access public
+ * @return $publisherURL
+ */
+ function getPublisherURL() {
+ return $this->publisherURL;
+ }
+
+ /**
+ * Release date, optional. If left blank, the time of the finalization will
+ * be used.
+ *
+ * A point or period of time associated with an event in the lifecycle of
+ * the resource.
+ *
+ * Date may be used to express temporal information at any level of
+ * granularity. Recommended best practice is to use an encoding scheme,
+ * such as the W3CDTF profile of ISO 8601 [W3CDTF].
+ *
+ * Used for the dc:date metadata parameter in the OPF file
+ *
+ * @param long $timestamp
+ * @access public
+ * @return bool $success
+ */
+ function setDate($timestamp) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->date = $timestamp;
+ $this->opf->date = $timestamp;
+ return TRUE;
+ }
+
+ /**
+ * Get Book date.
+ *
+ * @access public
+ * @return $date
+ */
+ function getDate() {
+ return $this->date;
+ }
+
+ /**
+ * Book (copy)rights, optional.
+ *
+ * Information about rights held in and over the resource.
+ *
+ * Typically, rights information includes a statement about various
+ * property rights associated with the resource, including intellectual
+ * property rights.
+ *
+ * Used for the dc:rights metadata parameter in the OPF file
+ *
+ * @param string $rightsText
+ * @access public
+ * @return bool $success
+ */
+ function setRights($rightsText) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->rights = $rightsText;
+ return TRUE;
+ }
+
+ /**
+ * Get Book rights.
+ *
+ * @access public
+ * @return $rights
+ */
+ function getRights() {
+ return $this->rights;
+ }
+
+ /**
+ * Add book Subject.
+ *
+ * The topic of the resource.
+ *
+ * Typically, the subject will be represented using keywords, key phrases,
+ * or classification codes. Recommended best practice is to use a
+ * controlled vocabulary. To describe the spatial or temporal topic of the
+ * resource, use the Coverage element.
+ *
+ * @param string $subject
+ */
+ function setSubject($subject) {
+ if ($this->isFinalized) {
+ return;
+ }
+ $this->opf->addDCMeta(DublinCore::SUBJECT, $this->decodeHtmlEntities($subject));
+ }
+
+ /**
+ * Book source URL, optional.
+ *
+ * A related resource from which the described resource is derived.
+ *
+ * The described resource may be derived from the related resource in whole
+ * or in part. Recommended best practice is to identify the related
+ * resource by means of a string conforming to a formal identification system.
+ *
+ * Used for the dc:source metadata parameter in the OPF file
+ *
+ * @param string $sourceURL
+ * @access public
+ * @return bool $success
+ */
+ function setSourceURL($sourceURL) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->sourceURL = $sourceURL;
+ return TRUE;
+ }
+
+ /**
+ * Get Book sourceURL.
+ *
+ * @access public
+ * @return $sourceURL
+ */
+ function getSourceURL() {
+ return $this->sourceURL;
+ }
+
+ /**
+ * Coverage, optional.
+ *
+ * The spatial or temporal topic of the resource, the spatial applicability
+ * of the resource, or the jurisdiction under which the resource is relevant.
+ *
+ * Spatial topic and spatial applicability may be a named place or a location
+ * specified by its geographic coordinates. Temporal topic may be a named
+ * period, date, or date range. A jurisdiction may be a named administrative
+ * entity or a geographic place to which the resource applies. Recommended
+ * best practice is to use a controlled vocabulary such as the Thesaurus of
+ * Geographic Names [TGN]. Where appropriate, named places or time periods
+ * can be used in preference to numeric identifiers such as sets of
+ * coordinates or date ranges.
+ *
+ * Used for the dc:coverage metadata parameter in the OPF file
+ *
+ * Same as ->addDublinCoreMetadata(DublinCore::COVERAGE, $coverage);
+ *
+ * @param string $coverage
+ * @access public
+ * @return bool $success
+ */
+ function setCoverage($coverage) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->coverage = $coverage;
+ return TRUE;
+ }
+
+ /**
+ * Get Book coverage.
+ *
+ * @access public
+ * @return $coverage
+ */
+ function getCoverage() {
+ return $this->coverage;
+ }
+
+ /**
+ * Set book Relation.
+ *
+ * A related resource.
+ *
+ * Recommended best practice is to identify the related resource by means
+ * of a string conforming to a formal identification system.
+ *
+ * @param string $relation
+ */
+ function setRelation($relation) {
+ if ($this->isFinalized) {
+ return;
+ }
+ $this->relation = $relation;
+ }
+
+ /**
+ * Get the book relation.
+ *
+ * @return string The relation.
+ */
+ function getRelation() {
+ return $this->relation;
+ }
+
+ /**
+ * Set book Generator.
+ *
+ * The generator is a meta tag added to the ncx file, it is not visible
+ * from within the book, but is a kind of electronic watermark.
+ *
+ * @param string $generator
+ */
+ function setGenerator($generator) {
+ if ($this->isFinalized) {
+ return;
+ }
+ $this->generator = $generator;
+ }
+
+ /**
+ * Get the book relation.
+ *
+ * @return string The generator identity string.
+ */
+ function getGenerator() {
+ return $this->generator;
+ }
+
+ /**
+ * Set ePub date formate to the short yyyy-mm-dd form, for compliance with
+ * a bug in EpubCheck, prior to its version 1.1.
+ *
+ * The latest version of ePubCheck can be obtained here:
+ * http://code.google.com/p/epubcheck/
+ *
+ * @access public
+ * @return bool $success
+ */
+ function setShortDateFormat() {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->dateformat = $this->dateformatShort;
+ return TRUE;
+ }
+
+ /**
+ * @Deprecated
+ */
+ function setIgnoreEmptyBuffer($ignoreEmptyBuffer = TRUE) {
+ die ("Function was deprecated, functionality is no longer needed.");
+ }
+
+ /**
+ * Set the references title for the ePub 3 landmarks section
+ *
+ * @param string $referencesTitle
+ * @param string $referencesId
+ * @param string $referencesClass
+ * @return bool
+ */
+ function setReferencesTitle($referencesTitle = "Guide", $referencesId = "", $referencesClass = "references") {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->ncx->referencesTitle = is_string($referencesTitle) ? trim($referencesTitle) : "Guide";
+ $this->ncx->referencesId = is_string($referencesId) ? trim($referencesId) : "references";
+ $this->ncx->referencesClass = is_string($referencesClass) ? trim($referencesClass) : "references";
+ return TRUE;
+ }
+
+ /**
+ * Set the references title for the ePub 3 landmarks section
+ *
+ * @param bool $referencesTitle
+ */
+ function setisReferencesAddedToToc($isReferencesAddedToToc = TRUE) {
+ if ($this->isFinalized) {
+ return FALSE;
+ }
+ $this->isReferencesAddedToToc = $isReferencesAddedToToc === TRUE;
+ return TRUE;
+ }
+
+ /**
+ * Get Book status.
+ *
+ * @access public
+ * @return bool
+ */
+ function isFinalized() {
+ return $this->isFinalized;
+ }
+
+ /**
+ * Build the Table of Contents. This is not strictly necessary, as most eReaders will build it from the navigation structure in the .ncx file.
+ *
+ * @param string $cssFileName Include a link to this css file in the TOC html.
+ * @param string $tocCSSClass The TOC is a " . str_repeat(" ", $level) . "sanitizeFileName($fileName) . "\">" . $chapterName . "
\n"; + } + } else if ($this->tocAddReferences === TRUE) { + if (array_key_exists($item, $this->ncx->referencesList)) { + $tocData .= "\t\n"; + } else if ($item === "toc") { + $tocData .= "\t\n"; + } else if ($item === "cover" && $this->isCoverImageSet) { + $tocData .= "\t\n"; + } + } + } + $tocData .= "Started: " . gmdate("D, d M Y H:i:s T", $this->tStart['sec']) . "\n Δ Start ; Δ Last ;"; + $this->logLine("Start"); + } + } + + function dumpInstalledModules() { + if ($this->isLogging) { + $isCurlInstalled = extension_loaded('curl') && function_exists('curl_version'); + $isGdInstalled = extension_loaded('gd') && function_exists('gd_info'); + $isExifInstalled = extension_loaded('exif') && function_exists('exif_imagetype'); + $isFileGetContentsInstalled = function_exists('file_get_contents'); + $isFileGetContentsExtInstalled = $isFileGetContentsInstalled && ini_get('allow_url_fopen'); + + $this->logLine("isCurlInstalled...............: " . ($isCurlInstalled ? "Yes" : "No")); + $this->logLine("isGdInstalled.................: " . ($isGdInstalled ? "Yes" : "No")); + $this->logLine("isExifInstalled...............: " . ($isExifInstalled ? "Yes" : "No")); + $this->logLine("isFileGetContentsInstalled....: " . ($isFileGetContentsInstalled ? "Yes" : "No")); + $this->logLine("isFileGetContentsExtInstalled.: " . ($isFileGetContentsExtInstalled ? "Yes" : "No")); + } + } + + function logLine($line) { + if ($this->isLogging) { + $tTemp = gettimeofday(); + $tS = $this->tStart['sec'] + (((int)($this->tStart['usec']/100))/10000); + $tL = $this->tLast['sec'] + (((int)($this->tLast['usec']/100))/10000); + $tT = $tTemp['sec'] + (((int)($tTemp['usec']/100))/10000); + + $logline = sprintf("\n+%08.04f; +%08.04f; ", ($tT-$tS), ($tT-$tL)) . $this->name . $line; + $this->log .= $logline; + $this->tLast = $tTemp; + + if ($this->isDebugging) { + echo "" . $bookEnd); // generation log + // Only used in case we need to debug EPub.php. + //$book->addChapter("ePubLog", "ePubLog.html", $content_start . $epuplog . "\n" . $bookEnd); + } + $book->finalize(); + $zipData = $book->sendBook($bookFileName); + } } diff --git a/inc/poche/global.inc.php b/inc/poche/global.inc.php index 14e9dd932..8cf86d03d 100755 --- a/inc/poche/global.inc.php +++ b/inc/poche/global.inc.php @@ -31,6 +31,11 @@ require_once INCLUDES . '/3rdparty/FlattrItem.class.php'; require_once INCLUDES . '/3rdparty/htmlpurifier/HTMLPurifier.auto.php'; +# epub library +require_once INCLUDES . '/3rdparty/libraries/PHPePub/Logger.php'; +require_once INCLUDES . '/3rdparty/libraries/PHPePub/EPub.php'; +require_once INCLUDES . '/3rdparty/libraries/PHPePub/EPubChapterSplitter.php'; + # Composer its autoloader for automatically loading Twig if (! file_exists(ROOT . '/vendor/autoload.php')) { Poche::$canRenderTemplates = false; diff --git a/index.php b/index.php index 9c943b1dc..79838ed91 100755 --- a/index.php +++ b/index.php @@ -70,6 +70,8 @@ if (isset($_GET['login'])) { $poche->createNewUser(); } elseif (isset($_GET['deluser'])) { $poche->deleteUser(); +} elseif (isset($_GET['epub'])) { + $poche->createEpub(); } elseif (isset($_GET['import'])) { $import = $poche->import(); $tpl_vars = array_merge($tpl_vars, $import); diff --git a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo index 2015f6158..fd0e23f64 100755 Binary files a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo and b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo differ diff --git a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po index 904a4178c..0b2a87dc8 100755 --- a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po +++ b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po @@ -1,19 +1,19 @@ msgid "" msgstr "" -"Project-Id-Version: wallabag 1.6.0\n" +"Project-Id-Version: wallabag 1.6.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-02-25 18:33+0300\n" +"POT-Creation-Date: 2014-05-10 20:09+0100\n" "PO-Revision-Date: \n" -"Last-Translator: Amaury Carrade" . $logline . "\n\n"; + } + } + } + + function getLog() { + return $this->log; + } +} +?> \ No newline at end of file diff --git a/inc/3rdparty/libraries/PHPePub/Zip.php b/inc/3rdparty/libraries/PHPePub/Zip.php new file mode 100644 index 000000000..01e035662 --- /dev/null +++ b/inc/3rdparty/libraries/PHPePub/Zip.php @@ -0,0 +1,818 @@ + + * @copyright 2009-2014 A. Grandt + * @license GNU LGPL 2.1 + * @link http://www.phpclasses.org/package/6110 + * @link https://github.com/Grandt/PHPZip + * @version 1.60 + */ +class Zip { + const VERSION = 1.60; + + const ZIP_LOCAL_FILE_HEADER = "\x50\x4b\x03\x04"; // Local file header signature + const ZIP_CENTRAL_FILE_HEADER = "\x50\x4b\x01\x02"; // Central file header signature + const ZIP_END_OF_CENTRAL_DIRECTORY = "\x50\x4b\x05\x06\x00\x00\x00\x00"; //end of Central directory record + + const EXT_FILE_ATTR_DIR = 010173200020; // Permission 755 drwxr-xr-x = (((S_IFDIR | 0755) << 16) | S_DOS_D); + const EXT_FILE_ATTR_FILE = 020151000040; // Permission 644 -rw-r--r-- = (((S_IFREG | 0644) << 16) | S_DOS_A); + + const ATTR_VERSION_TO_EXTRACT = "\x14\x00"; // Version needed to extract + const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version + + // Unix file types + const S_IFIFO = 0010000; // named pipe (fifo) + const S_IFCHR = 0020000; // character special + const S_IFDIR = 0040000; // directory + const S_IFBLK = 0060000; // block special + const S_IFREG = 0100000; // regular + const S_IFLNK = 0120000; // symbolic link + const S_IFSOCK = 0140000; // socket + + // setuid/setgid/sticky bits, the same as for chmod: + + const S_ISUID = 0004000; // set user id on execution + const S_ISGID = 0002000; // set group id on execution + const S_ISTXT = 0001000; // sticky bit + + // And of course, the other 12 bits are for the permissions, the same as for chmod: + // When addding these up, you can also just write the permissions as a simgle octal number + // ie. 0755. The leading 0 specifies octal notation. + const S_IRWXU = 0000700; // RWX mask for owner + const S_IRUSR = 0000400; // R for owner + const S_IWUSR = 0000200; // W for owner + const S_IXUSR = 0000100; // X for owner + const S_IRWXG = 0000070; // RWX mask for group + const S_IRGRP = 0000040; // R for group + const S_IWGRP = 0000020; // W for group + const S_IXGRP = 0000010; // X for group + const S_IRWXO = 0000007; // RWX mask for other + const S_IROTH = 0000004; // R for other + const S_IWOTH = 0000002; // W for other + const S_IXOTH = 0000001; // X for other + const S_ISVTX = 0001000; // save swapped text even after use + + // Filetype, sticky and permissions are added up, and shifted 16 bits left BEFORE adding the DOS flags. + + // DOS file type flags, we really only use the S_DOS_D flag. + + const S_DOS_A = 0000040; // DOS flag for Archive + const S_DOS_D = 0000020; // DOS flag for Directory + const S_DOS_V = 0000010; // DOS flag for Volume + const S_DOS_S = 0000004; // DOS flag for System + const S_DOS_H = 0000002; // DOS flag for Hidden + const S_DOS_R = 0000001; // DOS flag for Read Only + + private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB) + + private $zipData = NULL; + private $zipFile = NULL; + private $zipComment = NULL; + private $cdRec = array(); // central directory + private $offset = 0; + private $isFinalized = FALSE; + private $addExtraField = TRUE; + + private $streamChunkSize = 65536; + private $streamFilePath = NULL; + private $streamTimestamp = NULL; + private $streamFileComment = NULL; + private $streamFile = NULL; + private $streamData = NULL; + private $streamFileLength = 0; + private $streamExtFileAttr = null; + + /** + * Constructor. + * + * @param boolean $useZipFile Write temp zip data to tempFile? Default FALSE + */ + function __construct($useZipFile = FALSE) { + if ($useZipFile) { + $this->zipFile = tmpfile(); + } else { + $this->zipData = ""; + } + } + + function __destruct() { + if (is_resource($this->zipFile)) { + fclose($this->zipFile); + } + $this->zipData = NULL; + } + + /** + * Extra fields on the Zip directory records are Unix time codes needed for compatibility on the default Mac zip archive tool. + * These are enabled as default, as they do no harm elsewhere and only add 26 bytes per file added. + * + * @param bool $setExtraField TRUE (default) will enable adding of extra fields, anything else will disable it. + */ + function setExtraField($setExtraField = TRUE) { + $this->addExtraField = ($setExtraField === TRUE); + } + + /** + * Set Zip archive comment. + * + * @param string $newComment New comment. NULL to clear. + * @return bool $success + */ + public function setComment($newComment = NULL) { + if ($this->isFinalized) { + return FALSE; + } + $this->zipComment = $newComment; + + return TRUE; + } + + /** + * Set zip file to write zip data to. + * This will cause all present and future data written to this class to be written to this file. + * This can be used at any time, even after the Zip Archive have been finalized. Any previous file will be closed. + * Warning: If the given file already exists, it will be overwritten. + * + * @param string $fileName + * @return bool $success + */ + public function setZipFile($fileName) { + if (is_file($fileName)) { + unlink($fileName); + } + $fd=fopen($fileName, "x+b"); + if (is_resource($this->zipFile)) { + rewind($this->zipFile); + while (!feof($this->zipFile)) { + fwrite($fd, fread($this->zipFile, $this->streamChunkSize)); + } + + fclose($this->zipFile); + } else { + fwrite($fd, $this->zipData); + $this->zipData = NULL; + } + $this->zipFile = $fd; + + return TRUE; + } + + /** + * Add an empty directory entry to the zip archive. + * Basically this is only used if an empty directory is added. + * + * @param string $directoryPath Directory Path and name to be added to the archive. + * @param int $timestamp (Optional) Timestamp for the added directory, if omitted or set to 0, the current time will be used. + * @param string $fileComment (Optional) Comment to be added to the archive for this directory. To use fileComment, timestamp must be given. + * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. + * @return bool $success + */ + public function addDirectory($directoryPath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_DIR) { + if ($this->isFinalized) { + return FALSE; + } + $directoryPath = str_replace("\\", "/", $directoryPath); + $directoryPath = rtrim($directoryPath, "/"); + + if (strlen($directoryPath) > 0) { + $this->buildZipEntry($directoryPath.'/', $fileComment, "\x00\x00", "\x00\x00", $timestamp, "\x00\x00\x00\x00", 0, 0, $extFileAttr); + return TRUE; + } + return FALSE; + } + + /** + * Add a file to the archive at the specified location and file name. + * + * @param string $data File data. + * @param string $filePath Filepath and name to be used in the archive. + * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. + * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. + * @param bool $compress (Optional) Compress file, if set to FALSE the file will only be stored. Default TRUE. + * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. + * @return bool $success + */ + public function addFile($data, $filePath, $timestamp = 0, $fileComment = NULL, $compress = TRUE, $extFileAttr = self::EXT_FILE_ATTR_FILE) { + if ($this->isFinalized) { + return FALSE; + } + + if (is_resource($data) && get_resource_type($data) == "stream") { + $this->addLargeFile($data, $filePath, $timestamp, $fileComment, $extFileAttr); + return FALSE; + } + + $gzData = ""; + $gzType = "\x08\x00"; // Compression type 8 = deflate + $gpFlags = "\x00\x00"; // General Purpose bit flags for compression type 8 it is: 0=Normal, 1=Maximum, 2=Fast, 3=super fast compression. + $dataLength = strlen($data); + $fileCRC32 = pack("V", crc32($data)); + + if ($compress) { + $gzTmp = gzcompress($data); + $gzData = substr(substr($gzTmp, 0, strlen($gzTmp) - 4), 2); // gzcompress adds a 2 byte header and 4 byte CRC we can't use. + // The 2 byte header does contain useful data, though in this case the 2 parameters we'd be interrested in will always be 8 for compression type, and 2 for General purpose flag. + $gzLength = strlen($gzData); + } else { + $gzLength = $dataLength; + } + + if ($gzLength >= $dataLength) { + $gzLength = $dataLength; + $gzData = $data; + $gzType = "\x00\x00"; // Compression type 0 = stored + $gpFlags = "\x00\x00"; // Compression type 0 = stored + } + + if (!is_resource($this->zipFile) && ($this->offset + $gzLength) > $this->zipMemoryThreshold) { + $this->zipflush(); + } + + $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr); + + $this->zipwrite($gzData); + + return TRUE; + } + + /** + * Add the content to a directory. + * + * @author Adam Schmalhofer+ * @author A. Grandt + * + * @param string $realPath Path on the file system. + * @param string $zipPath Filepath and name to be used in the archive. + * @param bool $recursive Add content recursively, default is TRUE. + * @param bool $followSymlinks Follow and add symbolic links, if they are accessible, default is TRUE. + * @param array &$addedFiles Reference to the added files, this is used to prevent duplicates, efault is an empty array. + * If you start the function by parsing an array, the array will be populated with the realPath + * and zipPath kay/value pairs added to the archive by the function. + * @param bool $overrideFilePermissions Force the use of the file/dir permissions set in the $extDirAttr + * and $extFileAttr parameters. + * @param int $extDirAttr Permissions for directories. + * @param int $extFileAttr Permissions for files. + */ + public function addDirectoryContent($realPath, $zipPath, $recursive = TRUE, $followSymlinks = TRUE, &$addedFiles = array(), + $overrideFilePermissions = FALSE, $extDirAttr = self::EXT_FILE_ATTR_DIR, $extFileAttr = self::EXT_FILE_ATTR_FILE) { + if (file_exists($realPath) && !isset($addedFiles[realpath($realPath)])) { + if (is_dir($realPath)) { + if ($overrideFilePermissions) { + $this->addDirectory($zipPath, 0, null, $extDirAttr); + } else { + $this->addDirectory($zipPath, 0, null, self::getFileExtAttr($realPath)); + } + } + + $addedFiles[realpath($realPath)] = $zipPath; + + $iter = new DirectoryIterator($realPath); + foreach ($iter as $file) { + if ($file->isDot()) { + continue; + } + $newRealPath = $file->getPathname(); + $newZipPath = self::pathJoin($zipPath, $file->getFilename()); + + if (file_exists($newRealPath) && ($followSymlinks === TRUE || !is_link($newRealPath))) { + if ($file->isFile()) { + $addedFiles[realpath($newRealPath)] = $newZipPath; + if ($overrideFilePermissions) { + $this->addLargeFile($newRealPath, $newZipPath, 0, null, $extFileAttr); + } else { + $this->addLargeFile($newRealPath, $newZipPath, 0, null, self::getFileExtAttr($newRealPath)); + } + } else if ($recursive === TRUE) { + $this->addDirectoryContent($newRealPath, $newZipPath, $recursive, $followSymlinks, $addedFiles, $overrideFilePermissions, $extDirAttr, $extFileAttr); + } else { + if ($overrideFilePermissions) { + $this->addDirectory($zipPath, 0, null, $extDirAttr); + } else { + $this->addDirectory($zipPath, 0, null, self::getFileExtAttr($newRealPath)); + } + } + } + } + } + } + + /** + * Add a file to the archive at the specified location and file name. + * + * @param string $dataFile File name/path. + * @param string $filePath Filepath and name to be used in the archive. + * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. + * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. + * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. + * @return bool $success + */ + public function addLargeFile($dataFile, $filePath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_FILE) { + if ($this->isFinalized) { + return FALSE; + } + + if (is_string($dataFile) && is_file($dataFile)) { + $this->processFile($dataFile, $filePath, $timestamp, $fileComment, $extFileAttr); + } else if (is_resource($dataFile) && get_resource_type($dataFile) == "stream") { + $fh = $dataFile; + $this->openStream($filePath, $timestamp, $fileComment, $extFileAttr); + + while (!feof($fh)) { + $this->addStreamData(fread($fh, $this->streamChunkSize)); + } + $this->closeStream($this->addExtraField); + } + return TRUE; + } + + /** + * Create a stream to be used for large entries. + * + * @param string $filePath Filepath and name to be used in the archive. + * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. + * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. + * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. + * @return bool $success + */ + public function openStream($filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) { + if (!function_exists('sys_get_temp_dir')) { + die ("ERROR: Zip " . self::VERSION . " requires PHP version 5.2.1 or above if large files are used."); + } + + if ($this->isFinalized) { + return FALSE; + } + + $this->zipflush(); + + if (strlen($this->streamFilePath) > 0) { + $this->closeStream(); + } + + $this->streamFile = tempnam(sys_get_temp_dir(), 'Zip'); + $this->streamData = fopen($this->streamFile, "wb"); + $this->streamFilePath = $filePath; + $this->streamTimestamp = $timestamp; + $this->streamFileComment = $fileComment; + $this->streamFileLength = 0; + $this->streamExtFileAttr = $extFileAttr; + + return TRUE; + } + + /** + * Add data to the open stream. + * + * @param string $data + * @return mixed length in bytes added or FALSE if the archive is finalized or there are no open stream. + */ + public function addStreamData($data) { + if ($this->isFinalized || strlen($this->streamFilePath) == 0) { + return FALSE; + } + + $length = fwrite($this->streamData, $data, strlen($data)); + if ($length != strlen($data)) { + die (" Length mismatch
\n"); + } + $this->streamFileLength += $length; + + return $length; + } + + /** + * Close the current stream. + * + * @return bool $success + */ + public function closeStream() { + if ($this->isFinalized || strlen($this->streamFilePath) == 0) { + return FALSE; + } + + fflush($this->streamData); + fclose($this->streamData); + + $this->processFile($this->streamFile, $this->streamFilePath, $this->streamTimestamp, $this->streamFileComment, $this->streamExtFileAttr); + + $this->streamData = null; + $this->streamFilePath = null; + $this->streamTimestamp = null; + $this->streamFileComment = null; + $this->streamFileLength = 0; + $this->streamExtFileAttr = null; + + // Windows is a little slow at times, so a millisecond later, we can unlink this. + unlink($this->streamFile); + + $this->streamFile = null; + + return TRUE; + } + + private function processFile($dataFile, $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) { + if ($this->isFinalized) { + return FALSE; + } + + $tempzip = tempnam(sys_get_temp_dir(), 'ZipStream'); + + $zip = new ZipArchive; + if ($zip->open($tempzip) === TRUE) { + $zip->addFile($dataFile, 'file'); + $zip->close(); + } + + $file_handle = fopen($tempzip, "rb"); + $stats = fstat($file_handle); + $eof = $stats['size']-72; + + fseek($file_handle, 6); + + $gpFlags = fread($file_handle, 2); + $gzType = fread($file_handle, 2); + fread($file_handle, 4); + $fileCRC32 = fread($file_handle, 4); + $v = unpack("Vval", fread($file_handle, 4)); + $gzLength = $v['val']; + $v = unpack("Vval", fread($file_handle, 4)); + $dataLength = $v['val']; + + $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr); + + fseek($file_handle, 34); + $pos = 34; + + while (!feof($file_handle) && $pos < $eof) { + $datalen = $this->streamChunkSize; + if ($pos + $this->streamChunkSize > $eof) { + $datalen = $eof-$pos; + } + $data = fread($file_handle, $datalen); + $pos += $datalen; + + $this->zipwrite($data); + } + + fclose($file_handle); + + unlink($tempzip); + } + + /** + * Close the archive. + * A closed archive can no longer have new files added to it. + * + * @return bool $success + */ + public function finalize() { + if (!$this->isFinalized) { + if (strlen($this->streamFilePath) > 0) { + $this->closeStream(); + } + $cd = implode("", $this->cdRec); + + $cdRecSize = pack("v", sizeof($this->cdRec)); + $cdRec = $cd . self::ZIP_END_OF_CENTRAL_DIRECTORY + . $cdRecSize . $cdRecSize + . pack("VV", strlen($cd), $this->offset); + if (!empty($this->zipComment)) { + $cdRec .= pack("v", strlen($this->zipComment)) . $this->zipComment; + } else { + $cdRec .= "\x00\x00"; + } + + $this->zipwrite($cdRec); + + $this->isFinalized = TRUE; + $this->cdRec = NULL; + + return TRUE; + } + return FALSE; + } + + /** + * Get the handle ressource for the archive zip file. + * If the zip haven't been finalized yet, this will cause it to become finalized + * + * @return zip file handle + */ + public function getZipFile() { + if (!$this->isFinalized) { + $this->finalize(); + } + + $this->zipflush(); + + rewind($this->zipFile); + + return $this->zipFile; + } + + /** + * Get the zip file contents + * If the zip haven't been finalized yet, this will cause it to become finalized + * + * @return zip data + */ + public function getZipData() { + if (!$this->isFinalized) { + $this->finalize(); + } + if (!is_resource($this->zipFile)) { + return $this->zipData; + } else { + rewind($this->zipFile); + $filestat = fstat($this->zipFile); + return fread($this->zipFile, $filestat['size']); + } + } + + /** + * Send the archive as a zip download + * + * @param String $fileName The name of the Zip archive, in ISO-8859-1 (or ASCII) encoding, ie. "archive.zip". Optional, defaults to NULL, which means that no ISO-8859-1 encoded file name will be specified. + * @param String $contentType Content mime type. Optional, defaults to "application/zip". + * @param String $utf8FileName The name of the Zip archive, in UTF-8 encoding. Optional, defaults to NULL, which means that no UTF-8 encoded file name will be specified. + * @param bool $inline Use Content-Disposition with "inline" instead of "attached". Optional, defaults to FALSE. + * @return bool $success + */ + function sendZip($fileName = null, $contentType = "application/zip", $utf8FileName = null, $inline = false) { + if (!$this->isFinalized) { + $this->finalize(); + } + + $headerFile = null; + $headerLine = null; + if (!headers_sent($headerFile, $headerLine) or die("Error: Unable to send file $fileName. HTML Headers have already been sent from $headerFile in line $headerLine
")) { + if ((ob_get_contents() === FALSE || ob_get_contents() == '') or die("\nError: Unable to send file $fileName. Output buffer contains the following text (typically warnings or errors):
")) { + if (ini_get('zlib.output_compression')) { + ini_set('zlib.output_compression', 'Off'); + } + + header("Pragma: public"); + header("Last-Modified: " . gmdate("D, d M Y H:i:s T")); + header("Expires: 0"); + header("Accept-Ranges: bytes"); + header("Connection: close"); + header("Content-Type: " . $contentType); + $cd = "Content-Disposition: "; + if ($inline) { + $cd .= "inline"; + } else{ + $cd .= "attached"; + } + if ($fileName) { + $cd .= '; filename="' . $fileName . '"'; + } + if ($utf8FileName) { + $cd .= "; filename*=UTF-8''" . rawurlencode($utf8FileName); + } + header($cd); + header("Content-Length: ". $this->getArchiveSize()); + + if (!is_resource($this->zipFile)) { + echo $this->zipData; + } else { + rewind($this->zipFile); + + while (!feof($this->zipFile)) { + echo fread($this->zipFile, $this->streamChunkSize); + } + } + } + return TRUE; + } + return FALSE; + } + + /** + * Return the current size of the archive + * + * @return $size Size of the archive + */ + public function getArchiveSize() { + if (!is_resource($this->zipFile)) { + return strlen($this->zipData); + } + $filestat = fstat($this->zipFile); + + return $filestat['size']; + } + + /** + * Calculate the 2 byte dostime used in the zip entries. + * + * @param int $timestamp + * @return 2-byte encoded DOS Date + */ + private function getDosTime($timestamp = 0) { + $timestamp = (int)$timestamp; + $oldTZ = @date_default_timezone_get(); + date_default_timezone_set('UTC'); + $date = ($timestamp == 0 ? getdate() : getdate($timestamp)); + date_default_timezone_set($oldTZ); + if ($date["year"] >= 1980) { + return pack("V", (($date["mday"] + ($date["mon"] << 5) + (($date["year"]-1980) << 9)) << 16) | + (($date["seconds"] >> 1) + ($date["minutes"] << 5) + ($date["hours"] << 11))); + } + return "\x00\x00\x00\x00"; + } + + /** + * Build the Zip file structures + * + * @param string $filePath + * @param string $fileComment + * @param string $gpFlags + * @param string $gzType + * @param int $timestamp + * @param string $fileCRC32 + * @param int $gzLength + * @param int $dataLength + * @param int $extFileAttr Use self::EXT_FILE_ATTR_FILE for files, self::EXT_FILE_ATTR_DIR for Directories. + */ + private function buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr) { + $filePath = str_replace("\\", "/", $filePath); + $fileCommentLength = (empty($fileComment) ? 0 : strlen($fileComment)); + $timestamp = (int)$timestamp; + $timestamp = ($timestamp == 0 ? time() : $timestamp); + + $dosTime = $this->getDosTime($timestamp); + $tsPack = pack("V", $timestamp); + + $ux = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00"; + + if (!isset($gpFlags) || strlen($gpFlags) != 2) { + $gpFlags = "\x00\x00"; + } + + $isFileUTF8 = mb_check_encoding($filePath, "UTF-8") && !mb_check_encoding($filePath, "ASCII"); + $isCommentUTF8 = !empty($fileComment) && mb_check_encoding($fileComment, "UTF-8") && !mb_check_encoding($fileComment, "ASCII"); + if ($isFileUTF8 || $isCommentUTF8) { + $flag = 0; + $gpFlagsV = unpack("vflags", $gpFlags); + if (isset($gpFlagsV['flags'])) { + $flag = $gpFlagsV['flags']; + } + $gpFlags = pack("v", $flag | (1 << 11)); + } + + $header = $gpFlags . $gzType . $dosTime. $fileCRC32 + . pack("VVv", $gzLength, $dataLength, strlen($filePath)); // File name length + + $zipEntry = self::ZIP_LOCAL_FILE_HEADER; + $zipEntry .= self::ATTR_VERSION_TO_EXTRACT; + $zipEntry .= $header; + $zipEntry .= pack("v", ($this->addExtraField ? 28 : 0)); // Extra field length + $zipEntry .= $filePath; // FileName + // Extra fields + if ($this->addExtraField) { + $zipEntry .= "\x55\x54\x09\x00\x03" . $tsPack . $tsPack . $ux; + } + $this->zipwrite($zipEntry); + + $cdEntry = self::ZIP_CENTRAL_FILE_HEADER; + $cdEntry .= self::ATTR_MADE_BY_VERSION; + $cdEntry .= ($dataLength === 0 ? "\x0A\x00" : self::ATTR_VERSION_TO_EXTRACT); + $cdEntry .= $header; + $cdEntry .= pack("v", ($this->addExtraField ? 24 : 0)); // Extra field length + $cdEntry .= pack("v", $fileCommentLength); // File comment length + $cdEntry .= "\x00\x00"; // Disk number start + $cdEntry .= "\x00\x00"; // internal file attributes + $cdEntry .= pack("V", $extFileAttr); // External file attributes + $cdEntry .= pack("V", $this->offset); // Relative offset of local header + $cdEntry .= $filePath; // FileName + // Extra fields + if ($this->addExtraField) { + $cdEntry .= "\x55\x54\x05\x00\x03" . $tsPack . $ux; + } + if (!empty($fileComment)) { + $cdEntry .= $fileComment; // Comment + } + + $this->cdRec[] = $cdEntry; + $this->offset += strlen($zipEntry) + $gzLength; + } + + private function zipwrite($data) { + if (!is_resource($this->zipFile)) { + $this->zipData .= $data; + } else { + fwrite($this->zipFile, $data); + fflush($this->zipFile); + } + } + + private function zipflush() { + if (!is_resource($this->zipFile)) { + $this->zipFile = tmpfile(); + fwrite($this->zipFile, $this->zipData); + $this->zipData = NULL; + } + } + + /** + * Join $file to $dir path, and clean up any excess slashes. + * + * @param string $dir + * @param string $file + */ + public static function pathJoin($dir, $file) { + if (empty($dir) || empty($file)) { + return self::getRelativePath($dir . $file); + } + return self::getRelativePath($dir . '/' . $file); + } + + /** + * Clean up a path, removing any unnecessary elements such as /./, // or redundant ../ segments. + * If the path starts with a "/", it is deemed an absolute path and any /../ in the beginning is stripped off. + * The returned path will not end in a "/". + * + * Sometimes, when a path is generated from multiple fragments, + * you can get something like "../data/html/../images/image.jpeg" + * This will normalize that example path to "../data/images/image.jpeg" + * + * @param string $path The path to clean up + * @return string the clean path + */ + public static function getRelativePath($path) { + $path = preg_replace("#/+\.?/+#", "/", str_replace("\\", "/", $path)); + $dirs = explode("/", rtrim(preg_replace('#^(?:\./)+#', '', $path), '/')); + + $offset = 0; + $sub = 0; + $subOffset = 0; + $root = ""; + + if (empty($dirs[0])) { + $root = "/"; + $dirs = array_splice($dirs, 1); + } else if (preg_match("#[A-Za-z]:#", $dirs[0])) { + $root = strtoupper($dirs[0]) . "/"; + $dirs = array_splice($dirs, 1); + } + + $newDirs = array(); + foreach ($dirs as $dir) { + if ($dir !== "..") { + $subOffset--; + $newDirs[++$offset] = $dir; + } else { + $subOffset++; + if (--$offset < 0) { + $offset = 0; + if ($subOffset > $sub) { + $sub++; + } + } + } + } + + if (empty($root)) { + $root = str_repeat("../", $sub); + } + return $root . implode("/", array_slice($newDirs, 0, $offset)); + } + + /** + * Create the file permissions for a file or directory, for use in the extFileAttr parameters. + * + * @param int $owner Unix permisions for owner (octal from 00 to 07) + * @param int $group Unix permisions for group (octal from 00 to 07) + * @param int $other Unix permisions for others (octal from 00 to 07) + * @param bool $isFile + * @return EXTRERNAL_REF field. + */ + public static function generateExtAttr($owner = 07, $group = 05, $other = 05, $isFile = true) { + $fp = $isFile ? self::S_IFREG : self::S_IFDIR; + $fp |= (($owner & 07) << 6) | (($group & 07) << 3) | ($other & 07); + + return ($fp << 16) | ($isFile ? self::S_DOS_A : self::S_DOS_D); + } + + /** + * Get the file permissions for a file or directory, for use in the extFileAttr parameters. + * + * @param string $filename + * @return external ref field, or FALSE if the file is not found. + */ + public static function getFileExtAttr($filename) { + if (file_exists($filename)) { + $fp = fileperms($filename) << 16; + return $fp | (is_dir($filename) ? self::S_DOS_D : self::S_DOS_A); + } + return FALSE; + } +} +?> diff --git a/inc/3rdparty/libraries/PHPePub/lib.uuid.LICENCE.txt b/inc/3rdparty/libraries/PHPePub/lib.uuid.LICENCE.txt new file mode 100644 index 000000000..9424a83e7 --- /dev/null +++ b/inc/3rdparty/libraries/PHPePub/lib.uuid.LICENCE.txt @@ -0,0 +1,31 @@ + DrUUID RFC4122 library for PHP5 + by J. King (http://jkingweb.ca/) + Licensed under MIT license + + See http://jkingweb.ca/code/php/lib.uuid/ + for documentation + + Last revised 2010-02-15 + +Copyright (c) 2009 J. King + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/inc/3rdparty/libraries/PHPePub/lib.uuid.php b/inc/3rdparty/libraries/PHPePub/lib.uuid.php new file mode 100644 index 000000000..c6a8de523 --- /dev/null +++ b/inc/3rdparty/libraries/PHPePub/lib.uuid.php @@ -0,0 +1,314 @@ +string; + } + + public function __get($var) { + switch($var) { + case "bytes": + return $this->bytes; + case "hex": + return bin2hex($this->bytes); + case "string": + return $this->__toString(); + case "urn": + return "urn:uuid:".$this->__toString(); + case "version": + return ord($this->bytes[6]) >> 4; + case "variant": + $byte = ord($this->bytes[8]); + if ($byte >= self::varRes) { + return 3; + } + if ($byte >= self::varMS) { + return 2; + } + if ($byte >= self::varRFC) { + return 1; + } + return 0; + case "node": + if (ord($this->bytes[6])>>4==1) { + return bin2hex(substr($this->bytes,10)); + } else { + return NULL; + } + case "time": + if (ord($this->bytes[6])>>4==1) { + // Restore contiguous big-endian byte order + $time = bin2hex($this->bytes[6].$this->bytes[7].$this->bytes[4].$this->bytes[5].$this->bytes[0].$this->bytes[1].$this->bytes[2].$this->bytes[3]); + // Clear version flag + $time[0] = "0"; + // Do some reverse arithmetic to get a Unix timestamp + $time = (hexdec($time) - self::interval) / 10000000; + return $time; + } else { + return NULL; + } + default: + return NULL; + } + } + + protected function __construct($uuid) { + if (strlen($uuid) != 16) { + throw new UUIDException("Input must be a 128-bit integer."); + } + $this->bytes = $uuid; + // Optimize the most common use + $this->string = + bin2hex(substr($uuid,0,4))."-". + bin2hex(substr($uuid,4,2))."-". + bin2hex(substr($uuid,6,2))."-". + bin2hex(substr($uuid,8,2))."-". + bin2hex(substr($uuid,10,6)); + } + + protected static function mintTime($node = NULL) { + /* Generates a Version 1 UUID. + These are derived from the time at which they were generated. */ + // Get time since Gregorian calendar reform in 100ns intervals + // This is exceedingly difficult because of PHP's (and pack()'s) + // integer size limits. + // Note that this will never be more accurate than to the microsecond. + $time = microtime(1) * 10000000 + self::interval; + // Convert to a string representation + $time = sprintf("%F", $time); + preg_match("/^\d+/", $time, $time); //strip decimal point + // And now to a 64-bit binary representation + $time = base_convert($time[0], 10, 16); + $time = pack("H*", str_pad($time, 16, "0", STR_PAD_LEFT)); + // Reorder bytes to their proper locations in the UUID + $uuid = $time[4].$time[5].$time[6].$time[7].$time[2].$time[3].$time[0].$time[1]; + // Generate a random clock sequence + $uuid .= self::randomBytes(2); + // set variant + $uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC); + // set version + $uuid[6] = chr(ord($uuid[6]) & self::clearVer | self::version1); + // Set the final 'node' parameter, a MAC address + if ($node) { + $node = self::makeBin($node, 6); + } + if (!$node) { + // If no node was provided or if the node was invalid, + // generate a random MAC address and set the multicast bit + $node = self::randomBytes(6); + $node[0] = pack("C", ord($node[0]) | 1); + } + $uuid .= $node; + return $uuid; + } + + protected static function mintRand() { + /* Generate a Version 4 UUID. + These are derived soly from random numbers. */ + // generate random fields + $uuid = self::randomBytes(16); + // set variant + $uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC); + // set version + $uuid[6] = chr(ord($uuid[6]) & self::clearVer | self::version4); + return $uuid; + } + + protected static function mintName($ver, $node, $ns) { + /* Generates a Version 3 or Version 5 UUID. + These are derived from a hash of a name and its namespace, in binary form. */ + if (!$node) { + throw new UUIDException("A name-string is required for Version 3 or 5 UUIDs."); + } + // if the namespace UUID isn't binary, make it so + $ns = self::makeBin($ns, 16); + if (!$ns) { + throw new UUIDException("A binary namespace is required for Version 3 or 5 UUIDs."); + } + $uuid = null; + $version = self::version3; + switch($ver) { + case self::MD5: + $version = self::version3; + $uuid = md5($ns.$node,1); + break; + case self::SHA1: + $version = self::version5; + $uuid = substr(sha1($ns.$node,1),0, 16); + break; + } + // set variant + $uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC); + // set version + $uuid[6] = chr(ord($uuid[6]) & self::clearVer | $version); + return ($uuid); + } + + protected static function makeBin($str, $len) { + /* Insure that an input string is either binary or hexadecimal. + Returns binary representation, or false on failure. */ + if ($str instanceof self) { + return $str->bytes; + } + if (strlen($str)==$len) { + return $str; + } else { + $str = preg_replace("/^urn:uuid:/is", "", $str); // strip URN scheme and namespace + } + $str = preg_replace("/[^a-f0-9]/is", "", $str); // strip non-hex characters + if (strlen($str) != ($len * 2)) { + return FALSE; + } else { + return pack("H*", $str); + } + } + + public static function initRandom() { + /* Look for a system-provided source of randomness, which is usually crytographically secure. + /dev/urandom is tried first simply out of bias for Linux systems. */ + if (is_readable('/dev/urandom')) { + self::$randomSource = fopen('/dev/urandom', 'rb'); + self::$randomFunc = 'randomFRead'; + } + else if (class_exists('COM', 0)) { + try { + self::$randomSource = new COM('CAPICOM.Utilities.1'); // See http://msdn.microsoft.com/en-us/library/aa388182(VS.85).aspx + self::$randomFunc = 'randomCOM'; + } + catch(Exception $e) { + } + } + return self::$randomFunc; + } + + public static function randomBytes($bytes) { + return call_user_func(array('self', self::$randomFunc), $bytes); + } + + protected static function randomTwister($bytes) { + /* Get the specified number of random bytes, using mt_rand(). + Randomness is returned as a string of bytes. */ + $rand = ""; + for ($a = 0; $a < $bytes; $a++) { + $rand .= chr(mt_rand(0, 255)); + } + return $rand; + } + + protected static function randomFRead($bytes) { + /* Get the specified number of random bytes using a file handle + previously opened with UUID::initRandom(). + Randomness is returned as a string of bytes. */ + return fread(self::$randomSource, $bytes); + } + + protected static function randomCOM($bytes) { + /* Get the specified number of random bytes using Windows' + randomness source via a COM object previously created by UUID::initRandom(). + Randomness is returned as a string of bytes. */ + return base64_decode(self::$randomSource->GetRandom($bytes,0)); // straight binary mysteriously doesn't work, hence the base64 + } +} + +class UUIDException extends Exception { +} diff --git a/inc/poche/Poche.class.php b/inc/poche/Poche.class.php index 515d4cac3..92db7d1b2 100755 --- a/inc/poche/Poche.class.php +++ b/inc/poche/Poche.class.php @@ -1149,4 +1149,121 @@ class Poche return new HTMLPurifier($config); } + + /** + * handle epub + */ + public function createEpub() { + + switch ($_GET['method']) { + case 'id': + $entryID = filter_var($_GET['id'],FILTER_SANITIZE_NUMBER_INT); + $entry = $this->store->retrieveOneById($entryID, $this->user->getId()); + $entries = array($entry); + $bookTitle = $entry['title']; + $bookFileName = substr($bookTitle, 0, 200); + break; + case 'all': + $entries = $this->store->retrieveAll($this->user->getId()); + $bookTitle = sprintf(_('All my articles on '), date(_('d.m.y'))); #translatable because each country has it's own date format system + $bookFileName = _('Allarticles') . date(_('dmY')); + break; + case 'tag': + $tag = filter_var($_GET['tag'],FILTER_SANITIZE_STRING); + $tags_id = $this->store->retrieveAllTags($this->user->getId(),$tag); + $tag_id = $tags_id[0]["id"]; // we take the first result, which is supposed to match perfectly. There must be a workaround. + $entries = $this->store->retrieveEntriesByTag($tag_id,$this->user->getId()); + $bookTitle = sprintf(_('Articles tagged %s'),$tag); + $bookFileName = substr(sprintf(_('Tag %s'),$tag), 0, 200); + break; + case 'category': + $category = filter_var($_GET['category'],FILTER_SANITIZE_STRING); + $entries = $this->store->getEntriesByView($category,$this->user->getId()); + $bookTitle = sprintf(_('All articles in category %s'), $category); + $bookFileName = substr(sprintf(_('Category %s'),$category), 0, 200); + break; + case 'search': + $search = filter_var($_GET['search'],FILTER_SANITIZE_STRING); + $entries = $this->store->search($search,$this->user->getId()); + $bookTitle = sprintf(_('All articles for search %s'), $search); + $bookFileName = substr(sprintf(_('Search %s'), $search), 0, 200); + break; + case 'default': + die(_('Uh, there is a problem while generating epub.')); + + } + + $content_start = + "\n" + . "\n" + . "" + . "\n" + . "
" . htmlentities(ob_get_contents()) . "wallabag articles book \n" + . "\n" + . "\n"; + + $bookEnd = "\n\n"; + + $log = new Logger("wallabag", TRUE); + $fileDir = CACHE; + + + $book = new EPub(EPub::BOOK_VERSION_EPUB3); + $log->logLine("new EPub()"); + $log->logLine("EPub class version: " . EPub::VERSION); + $log->logLine("EPub Req. Zip version: " . EPub::REQ_ZIP_VERSION); + $log->logLine("Zip version: " . Zip::VERSION); + $log->logLine("getCurrentServerURL: " . $book->getCurrentServerURL()); + $log->logLine("getCurrentPageURL..: " . $book->getCurrentPageURL()); + + $book->setTitle(_('wallabag\'s articles')); + $book->setIdentifier("http://$_SERVER[HTTP_HOST]", EPub::IDENTIFIER_URI); // Could also be the ISBN number, prefered for published books, or a UUID. + //$book->setLanguage("en"); // Not needed, but included for the example, Language is mandatory, but EPub defaults to "en". Use RFC3066 Language codes, such as "en", "da", "fr" etc. + $book->setDescription(_("Some articles saved on my wallabag")); + $book->setAuthor("wallabag","wallabag"); + $book->setPublisher("wallabag","wallabag"); // I hope this is a non existant address :) + $book->setDate(time()); // Strictly not needed as the book date defaults to time(). + //$book->setRights("Copyright and licence information specific for the book."); // As this is generated, this _could_ contain the name or licence information of the user who purchased the book, if needed. If this is used that way, the identifier must also be made unique for the book. + $book->setSourceURL("http://$_SERVER[HTTP_HOST]"); + + $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "PHP"); + $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "wallabag"); + + $cssData = "body {\n margin-left: .5em;\n margin-right: .5em;\n text-align: justify;\n}\n\np {\n font-family: serif;\n font-size: 10pt;\n text-align: justify;\n text-indent: 1em;\n margin-top: 0px;\n margin-bottom: 1ex;\n}\n\nh1, h2 {\n font-family: sans-serif;\n font-style: italic;\n text-align: center;\n background-color: #6b879c;\n color: white;\n width: 100%;\n}\n\nh1 {\n margin-bottom: 2px;\n}\n\nh2 {\n margin-top: -2px;\n margin-bottom: 2px;\n}\n"; + + $log->logLine("Add Cover"); + + $fullTitle = "" . $bookTitle . "
\n"; + + $book->setCoverImage("Cover.png", file_get_contents("themes/baggy/img/apple-touch-icon-152.png"), "image/png", $fullTitle); + + $cover = $content_start . _('Produced by wallabag with PHPePub') . $bookEnd; + + //$book->addChapter("Table of Contents", "TOC.xhtml", NULL, false, EPub::EXTERNAL_REF_IGNORE); + $book->addChapter("Notices", "Cover2.html", $cover); + + $book->buildTOC(); + + foreach ($entries as $entry) { + $tags = $this->store->retrieveTagsByEntry($entry['id']); + foreach ($tags as $tag) { + $book->setSubject($tag['value']); + } + + $log->logLine("Set up parameters"); + + $chapter = $content_start . $entry['content'] . $bookEnd; + $book->addChapter($entry['title'], htmlspecialchars($entry['title']) . ".html", $chapter, true, EPub::EXTERNAL_REF_ADD); + $log->logLine("Added chapter " . $entry['title']); + } + + if (DEBUG_POCHE) { + $epuplog = $book->getLog(); + $book->addChapter("Log", "Log.html", $content_start . $log->getLog() . "\n
IMPORT_LIMIT"
+"code> (nombre d'éléments téléchargés à la fois) et IMPORT_DELAY
"
+"(le délai d'attente entre deux séquences de téléchargement)."
-msgid "Import from Pocket"
-msgstr "Import depuis Pocket"
+msgid "File:"
+msgstr "Fichier: "
-#, php-format
-msgid "(you must have a %s file on your server)"
-msgstr "(le fichier %s doit être présent sur le serveur)"
-
-msgid "Import from Readability"
-msgstr "Importer depuis Readability"
-
-msgid "Import from Instapaper"
-msgstr "Importer depuis Instapaper"
-
-msgid "Import from wallabag"
-msgstr "Importer depuis wallabag"
+msgid "You can click here to fetch content for articles with no content."
+msgstr ""
+"Vous pouvez cliquer ici pour télécharger le contenu des articles vides."
msgid "Export your wallabag data"
msgstr "Exporter vos données de wallabag"
@@ -175,110 +283,50 @@ msgstr "Cache"
msgid "to delete cache."
msgstr "pour effacer le cache."
-msgid "You can enter multiple tags, separated by commas."
-msgstr "Vous pouvez entrer plusieurs tags, séparés par des virgules."
+msgid "Add user"
+msgstr "Ajouter un utilisateur"
-msgid "return to article"
-msgstr "retourner à l'article"
+msgid "Add a new user :"
+msgstr "Ajouter un nouvel utilisateur: "
-msgid "plop"
-msgstr "plop"
-
-msgid "You can check your configuration here."
-msgstr "Vous pouvez vérifier votre configuration ici."
-
-msgid "favoris"
-msgstr "favoris"
-
-msgid "archive"
-msgstr "archive"
-
-msgid "unread"
-msgstr "non lus"
-
-msgid "by date asc"
-msgstr "par date asc"
-
-msgid "by date"
-msgstr "par date"
-
-msgid "by date desc"
-msgstr "par date desc"
-
-msgid "by title asc"
-msgstr "par titre asc"
-
-msgid "by title"
-msgstr "par titre"
-
-msgid "by title desc"
-msgstr "par titre desc"
-
-msgid "Tag"
-msgstr "Tag"
-
-msgid "No articles found."
-msgstr "Aucun article trouvé."
-
-msgid "Toggle mark as read"
-msgstr "Marquer comme lu / non lu"
-
-msgid "toggle favorite"
-msgstr "marquer / enlever comme favori"
-
-msgid "delete"
-msgstr "supprimer"
-
-msgid "original"
-msgstr "original"
-
-msgid "estimated reading time:"
-msgstr "temps de lecture estimé :"
-
-msgid "mark all the entries as read"
-msgstr "marquer tous les articles comme lus"
-
-msgid "results"
-msgstr "résultats"
-
-msgid "installation"
-msgstr "installation"
-
-msgid "install your wallabag"
-msgstr "installez votre wallabag"
-
-msgid "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to read the documentation on wallabag website."
-msgstr "wallabag n'est pas encore installé. Merci de remplir le formulaire suivant pour l'installer. N'hésitez pas à lire la documentation sur le site de wallabag."
+msgid "Login for new user"
+msgstr "Identifiant du nouvel utilisateur"
msgid "Login"
msgstr "Nom d'utilisateur"
-msgid "Repeat your password"
-msgstr "Répétez votre mot de passe"
+msgid "Password for new user"
+msgstr "Mot de passe du nouvel utilisateur: "
-msgid "Install"
-msgstr "Installer"
+msgid "Send"
+msgstr "Imvoyer"
-msgid "login to your wallabag"
-msgstr "se connecter à votre wallabag"
+msgid "Delete account"
+msgstr "Supprimer le compte"
-msgid "Login to wallabag"
-msgstr "Se connecter à wallabag"
+msgid "You can delete your account by entering your password and validating."
+msgstr ""
+"Vous pouvez supprimer votre compte en entrant votre mot de passe et en "
+"validant."
-msgid "you are in demo mode, some features may be disabled."
-msgstr "vous êtes en mode démo, certaines fonctionnalités peuvent être désactivées."
+msgid "Be careful, data will be erased forever (that is a very long time)."
+msgstr "Attention, les données seront perdues pour toujours."
-msgid "Username"
-msgstr "Nom d'utilisateur"
+msgid "Type here your password"
+msgstr "Entrez votre mot de passe ici"
-msgid "Stay signed in"
-msgstr "Rester connecté"
+msgid "You are the only user, you cannot delete your own account."
+msgstr ""
+"Vous êtes l'unique utilisateur, vous ne pouvez pas supprimer votre compte."
-msgid "(Do not check on public computers)"
-msgstr "(Ne pas cocher sur un ordinateur public)"
+msgid ""
+"To completely remove wallabag, delete the wallabag folder on your web server."
+msgstr ""
+"Pour déinstaller complètement Wallabag, supprimez le répertoire "
+"wallabag
de votre serveur Web."
-msgid "Sign in"
-msgstr "Se connecter"
+msgid "Save a link"
+msgstr "Ajouter un lien"
msgid "Return home"
msgstr "Retour accueil"
@@ -310,6 +358,9 @@ msgstr "Shaarli"
msgid "flattr"
msgstr "Flattr"
+msgid "Print"
+msgstr "Imprimer"
+
msgid "Does this article appear wrong?"
msgstr "Cet article s'affiche mal ?"
@@ -319,54 +370,18 @@ msgstr "tags :"
msgid "Edit tags"
msgstr "Modifier les tags"
-msgid "save link!"
-msgstr "enregistrer le lien !"
-
-msgid "powered by"
-msgstr "propulsé par"
-
-msgid "debug mode is on so cache is off."
-msgstr "le mode de debug est actif, le cache est donc désactivé."
-
-msgid "your wallabag version:"
-msgstr "votre version de wallabag :"
-
-msgid "storage:"
-msgstr "stockage :"
-
-msgid "home"
-msgstr "accueil"
-
-msgid "favorites"
+msgid "favoris"
msgstr "favoris"
-msgid "tags"
-msgstr "tags"
+msgid "mark all the entries as read"
+msgstr "marquer tous les articles comme lus"
-msgid "save a link"
-msgstr "sauver un lien"
+msgid "toggle view mode"
+msgstr "changer de mode de visualisation"
-msgid "logout"
-msgstr "déconnexion"
-
-msgid "back to home"
+msgid "return home"
msgstr "retour à l'accueil"
-msgid "toggle mark as read"
-msgstr "marquer comme lu / non lu"
-
-msgid "tweet"
-msgstr "tweet"
-
-msgid "email"
-msgstr "e-mail"
-
-msgid "this article appears wrong?"
-msgstr "cet article s'affiche mal ?"
-
-msgid "No link available here!"
-msgstr "Aucun lien n'est disponible ici !"
-
msgid "Poching a link"
msgstr "Enregistrer un lien"
@@ -395,7 +410,9 @@ msgid "a more recent development version is available."
msgstr "une version de développement plus récente est disponible."
msgid "Please execute the import script locally, it can take a very long time."
-msgstr "Merci d'exécuter le script d'importation en local car cela peut prendre du temps."
+msgstr ""
+"Merci d'exécuter le script d'importation en local car cela peut prendre du "
+"temps."
msgid "More infos in the official doc:"
msgstr "Plus d'infos dans la documentation officielle :"
@@ -403,21 +420,146 @@ msgstr "Plus d'infos dans la documentation officielle :"
msgid "import from Pocket"
msgstr "importation depuis Pocket"
+#, php-format
+msgid "(you must have a %s file on your server)"
+msgstr "(le fichier %s doit être présent sur le serveur)"
+
msgid "import from Readability"
msgstr "importation depuis Readability"
msgid "import from Instapaper"
msgstr "importation depuis Instapaper"
-msgid "estimated reading time :"
-msgstr "temps de lecture estimé :"
+msgid "Start typing for auto complete."
+msgstr "Commencez à taper pour activer l'auto-complétion."
-msgid "Mark all the entries as read"
-msgstr "Marquer tous les articles comme lus"
+msgid "You can enter multiple tags, separated by commas."
+msgstr "Vous pouvez entrer plusieurs tags, séparés par des virgules."
+
+msgid "return to article"
+msgstr "retourner à l'article"
+
+msgid "by date asc"
+msgstr "par date asc"
+
+msgid "by date"
+msgstr "par date"
+
+msgid "by date desc"
+msgstr "par date desc"
+
+msgid "by title asc"
+msgstr "par titre asc"
+
+msgid "by title"
+msgstr "par titre"
+
+msgid "by title desc"
+msgstr "par titre desc"
+
+msgid "home"
+msgstr "accueil"
+
+msgid "tags"
+msgstr "tags"
+
+msgid "save a link"
+msgstr "sauver un lien"
+
+msgid "search"
+msgstr "rechercher"
+
+msgid "logout"
+msgstr "déconnexion"
+
+msgid "installation"
+msgstr "installation"
+
+msgid "install your wallabag"
+msgstr "installez votre wallabag"
+
+msgid ""
+"wallabag is still not installed. Please fill the below form to install it. "
+"Don't hesitate to read the documentation "
+"on wallabag website."
+msgstr ""
+"wallabag n'est pas encore installé. Merci de remplir le formulaire suivant "
+"pour l'installer. N'hésitez pas à lire la "
+"documentation sur le site de wallabag."
+
+msgid "Repeat your password"
+msgstr "Répétez votre mot de passe"
+
+msgid "Install"
+msgstr "Installer"
+
+msgid ""
+"You can check your configuration "
+"here."
+msgstr ""
+"Vous pouvez vérifier votre configuration ici."
msgid "Tags"
msgstr "Tags"
+msgid "No link available here!"
+msgstr "Aucun lien n'est disponible ici !"
+
+msgid "toggle mark as read"
+msgstr "marquer comme lu / non lu"
+
+msgid "tweet"
+msgstr "tweet"
+
+msgid "email"
+msgstr "e-mail"
+
+msgid "this article appears wrong?"
+msgstr "cet article s'affiche mal ?"
+
+msgid "Search"
+msgstr "Rechercher"
+
+msgid "Download required for "
+msgstr "Téléchargement requis pour "
+
+msgid "records"
+msgstr " éléments."
+
+msgid "Downloading next "
+msgstr "Téléchargement des "
+
+msgid "articles, please wait"
+msgstr " prochains éléments, veuillez patienter"
+
+msgid "Enter your search here"
+msgstr "Entrez votre recherche ici"
+
+#, php-format
+msgid ""
+"The new user %s has been installed. Do you want to logout ?"
+msgstr ""
+"Le nouvel utilisateur « %s » a été ajouté. Voulez-vous vous déconnecter ?"
+
+#, php-format
+msgid "Error : An user with the name %s already exists !"
+msgstr "Erreur: Un utilisateur avec le nom « %s » existe déjà."
+
+#, php-format
+msgid "User %s has been successfully deleted !"
+msgstr "L'utilisateur « %s » a bien été supprimé !"
+
+msgid "Error : The password is wrong !"
+msgstr "Erreur: Le mot de passe est incorrect !"
+
+msgid "Error : You are the only user, you cannot delete your account !"
+msgstr ""
+"Erreur: Vous êtes l'unique utilisateur, vous ne pouvez pas supprimer votre "
+"compte !"
+
msgid "Untitled"
msgstr "Sans titre"
@@ -448,11 +590,15 @@ msgstr "en mode démo, vous ne pouvez pas mettre à jour le mot de passe"
msgid "your password has been updated"
msgstr "votre mot de passe a été mis à jour"
-msgid "the two fields have to be filled & the password must be the same in the two fields"
-msgstr "les deux champs doivent être remplis & le mot de passe doit être le même dans les deux"
+msgid ""
+"the two fields have to be filled & the password must be the same in the two "
+"fields"
+msgstr ""
+"les deux champs doivent être remplis & le mot de passe doit être le même "
+"dans les deux"
msgid "still using the \""
-msgstr "utilise encore \""
+msgstr "Vous utilisez toujours \""
msgid "that theme does not seem to be installed"
msgstr "ce thème ne semble pas installé"
@@ -475,26 +621,26 @@ msgstr "bienvenue dans votre wallabag"
msgid "login failed: bad login or password"
msgstr "échec de l'identification : mauvais identifiant ou mot de passe"
-msgid "import from instapaper completed"
-msgstr "Importation depuis Instapaper achevée"
+msgid "Untitled - Import - "
+msgstr "Sans titre - Importer - "
-msgid "import from pocket completed"
-msgstr "Importation depuis Pocket achevée"
+msgid "click to finish import"
+msgstr "cliquez pour terminer l'importation"
-msgid "import from Readability completed. "
-msgstr "Importation depuis Readability achevée"
+msgid "Articles inserted: "
+msgstr "Articles ajoutés: "
-msgid "import from Poche completed. "
-msgstr "Importation depuis Pocket achevée"
+msgid ". Please note, that some may be marked as \"read\"."
+msgstr ". Notez que certains pourraient être marqués comme lus."
-msgid "Unknown import provider."
-msgstr "Format d'importation inconnu."
+msgid "Import finished."
+msgstr "Importation terminée."
-msgid "Incomplete inc/poche/define.inc.php file, please define \""
-msgstr "Fichier inc/poche/define.inc.php incomplet, merci de définir \""
+msgid "Undefined"
+msgstr "Non définit"
-msgid "Could not find required \""
-msgstr "Impossible de trouver \""
+msgid "User with this id ("
+msgstr "Utilisateur avec cet identifiant ("
msgid "Uh, there is a problem while generating feeds."
msgstr "Hum, il y a un problème lors de la génération des flux."
@@ -505,11 +651,47 @@ msgstr "Cache effacé."
msgid "Oops, it seems you don't have PHP 5."
msgstr "Oups, vous ne semblez pas avoir PHP 5."
-msgid "search"
-msgstr "rechercher"
+#~ msgid ""
+#~ "Please execute the import script locally as it can take a very long time."
+#~ msgstr ""
+#~ "Merci d'exécuter le script d'importation en local car cela peut prendre "
+#~ "du temps."
-msgid "Search"
-msgstr "Rechercher"
+#~ msgid "More info in the official documentation:"
+#~ msgstr "Plus d'infos dans la documentation officielle :"
+
+#~ msgid "Import from Pocket"
+#~ msgstr "Import depuis Pocket"
+
+#~ msgid "Import from Readability"
+#~ msgstr "Importer depuis Readability"
+
+#~ msgid "Import from Instapaper"
+#~ msgstr "Importer depuis Instapaper"
+
+#~ msgid "Import from wallabag"
+#~ msgstr "Importer depuis wallabag"
+
+#~ msgid "import from instapaper completed"
+#~ msgstr "Importation depuis Instapaper achevée"
+
+#~ msgid "import from pocket completed"
+#~ msgstr "Importation depuis Pocket achevée"
+
+#~ msgid "import from Readability completed. "
+#~ msgstr "Importation depuis Readability achevée"
+
+#~ msgid "import from Poche completed. "
+#~ msgstr "Importation depuis Pocket achevée"
+
+#~ msgid "Unknown import provider."
+#~ msgstr "Format d'importation inconnu."
+
+#~ msgid "Incomplete inc/poche/define.inc.php file, please define \""
+#~ msgstr "Fichier inc/poche/define.inc.php incomplet, merci de définir \""
+
+#~ msgid "Could not find required \""
+#~ msgstr "Impossible de trouver \""
#~ msgid "poche it!"
#~ msgstr "pochez-le !"
diff --git a/themes/baggy/config.twig b/themes/baggy/config.twig
index 29d9e0483..46735f074 100755
--- a/themes/baggy/config.twig
+++ b/themes/baggy/config.twig
@@ -124,6 +124,10 @@
{% if constant('STORAGE') == 'sqlite' %}
{% trans "Click here" %} {% trans "to download your database." %}
{% endif %}
{% trans "Click here" %} {% trans "to export your wallabag data." %}
+
+ {% trans "Fancy an E-Book ?" %}
+ {% trans "Click on this link to get all your articles in one ebook (ePub 3 format)." %}
+
{% trans "This can take a while and can even fail if you have too many articles, depending on your server configuration." %}
{% trans "Cache" %}
{% trans "Click here" %} {% trans "to delete cache." %}
diff --git a/themes/baggy/home.twig b/themes/baggy/home.twig
index 8df5ce1ca..3942d3bf8 100755
--- a/themes/baggy/home.twig
+++ b/themes/baggy/home.twig
@@ -56,8 +56,14 @@
{% endfor %}
{% trans "Click here" %} {% trans "to export your wallabag data." %}
+{% trans "Click on this link to get all your articles in one ebook (ePub 3 format)." %}
+
{% trans "This can take a while and can even fail if you have too many articles, depending on your server configuration." %}
{% trans 'Add a new user :' %}
diff --git a/themes/courgette/home.twig b/themes/courgette/home.twig index 6ba72d353..401f3f20d 100755 --- a/themes/courgette/home.twig +++ b/themes/courgette/home.twig @@ -50,6 +50,13 @@{{ entry.content|striptags|slice(0, 300) }}...
{% endfor %} - {% endif %} + {{ block('pager') }} + + {% if tag %}{% trans "Download the articles from this tag in an epub" %} + {% elseif search_term is defined %}{% trans "Download the articles from this search in an epub" %} + {% else %}{% trans "Download the articles from this category in an epub" %}{% endif %} + + {% endif %} + {% endblock %} \ No newline at end of file diff --git a/themes/default/config.twig b/themes/default/config.twig index 5ed9d80f8..160f6046a 100755 --- a/themes/default/config.twig +++ b/themes/default/config.twig @@ -127,6 +127,10 @@{% trans "Click here" %} {% trans "to delete cache." %}
+{% trans "Click on this link to get all your articles in one ebook (ePub 3 format)." %}
+
{% trans "This can take a while and can even fail if you have too many articles, depending on your server configuration." %}
{% trans 'Add a new user :' %}