X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=src%2FWallabag%2FCoreBundle%2FHelper%2FEntriesExport.php;h=31a80d6e3283b12365f7fe2e228a20ffcde372b4;hb=5e1f27767bc2dcf0760bc3061544ecbb833ad5e7;hp=fad0bb977d8822597b0a181d8f7f9e86140ef1d0;hpb=03690d138792dde6405e3d2eb3c53f6572eb3c43;p=github%2Fwallabag%2Fwallabag.git diff --git a/src/Wallabag/CoreBundle/Helper/EntriesExport.php b/src/Wallabag/CoreBundle/Helper/EntriesExport.php index fad0bb97..64591687 100644 --- a/src/Wallabag/CoreBundle/Helper/EntriesExport.php +++ b/src/Wallabag/CoreBundle/Helper/EntriesExport.php @@ -2,92 +2,131 @@ namespace Wallabag\CoreBundle\Helper; +use Html2Text\Html2Text; +use JMS\Serializer\SerializationContext; +use JMS\Serializer\SerializerBuilder; use PHPePub\Core\EPub; use PHPePub\Core\Structure\OPF\DublinCore; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Translation\TranslatorInterface; +use Wallabag\CoreBundle\Entity\Entry; +/** + * This class doesn't have unit test BUT it's fully covered by a functional test with ExportControllerTest. + */ class EntriesExport { - private $format; - private $method; - private $title; - private $entries; - private $authors = array('wallabag'); - private $language; - private $tags; - - public function __construct($entries) + private $wallabagUrl; + private $logoPath; + private $translator; + private $title = ''; + private $entries = []; + private $author = 'wallabag'; + private $language = ''; + + /** + * @param TranslatorInterface $translator Translator service + * @param string $wallabagUrl Wallabag instance url + * @param string $logoPath Path to the logo FROM THE BUNDLE SCOPE + */ + public function __construct(TranslatorInterface $translator, $wallabagUrl, $logoPath) { - $this->entries = $entries; + $this->translator = $translator; + $this->wallabagUrl = $wallabagUrl; + $this->logoPath = $logoPath; + } - foreach ($entries as $entry) { - $this->tags[] = $entry->getTags(); - } - if (count($entries) === 1) { - $this->language = $entries[0]->getLanguage(); + /** + * Define entries. + * + * @param array|Entry $entries An array of entries or one entry + * + * @return EntriesExport + */ + public function setEntries($entries) + { + if (!\is_array($entries)) { + $this->language = $entries->getLanguage(); + $entries = [$entries]; } + + $this->entries = $entries; + + return $this; } /** * Sets the category of which we want to get articles, or just one entry. * * @param string $method Method to get articles + * + * @return EntriesExport */ - public function setMethod($method) + public function updateTitle($method) { - $this->method = $method; - - switch ($this->method) { - case 'All': - $this->title = 'All Articles'; - break; - case 'Unread': - $this->title = 'Unread articles'; - break; - case 'Starred': - $this->title = 'Starred articles'; - break; - case 'Archive': - $this->title = 'Archived articles'; - break; - case 'entry': - $this->title = $this->entries[0]->getTitle(); - break; - default: - break; + $this->title = $method . ' articles'; + + if ('entry' === $method) { + $this->title = $this->entries[0]->getTitle(); } + + return $this; } /** - * Sets the output format. + * Sets the author for one entry or category. * - * @param string $format + * The publishers are used, or the domain name if empty. + * + * @param string $method Method to get articles + * + * @return EntriesExport */ - public function exportAs($format) + public function updateAuthor($method) { - $this->format = $format; + if ('entry' !== $method) { + $this->author = 'Various authors'; - switch ($this->format) { - case 'epub': - $this->produceEpub(); - break; + return $this; + } - case 'mobi': - $this->produceMobi(); - break; + $this->author = $this->entries[0]->getDomainName(); - case 'pdf': - $this->producePDF(); - break; + $publishedBy = $this->entries[0]->getPublishedBy(); + if (!empty($publishedBy)) { + $this->author = implode(', ', $publishedBy); + } - case 'csv': - $this->produceCSV(); - break; + return $this; + } - default: - break; + /** + * Sets the output format. + * + * @param string $format + * + * @return Response + */ + public function exportAs($format) + { + $functionName = 'produce' . ucfirst($format); + if (method_exists($this, $functionName)) { + return $this->$functionName(); } + + throw new \InvalidArgumentException(sprintf('The format "%s" is not yet supported.', $format)); + } + + public function exportJsonData() + { + return $this->prepareSerializingContent('json'); } + /** + * Use PHPePub to dump a .epub file. + * + * @return Response + */ private function produceEpub() { /* @@ -95,12 +134,12 @@ class EntriesExport */ $content_start = "\n" - ."\n" - .'' - ."\n" - .''._('wallabag articles book')."\n" - ."\n" - ."\n"; + . "\n" + . '' + . "\n" + . "wallabag articles book\n" + . "\n" + . "\n"; $bookEnd = "\n\n"; @@ -111,17 +150,17 @@ class EntriesExport */ $book->setTitle($this->title); - $book->setIdentifier($this->title, EPub::IDENTIFIER_URI); // Could also be the ISBN number, prefered for published books, or a UUID. - $book->setLanguage($this->language); // 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')); + // 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->setLanguage($this->language); + $book->setDescription('Some articles saved on my wallabag'); - foreach ($this->authors as $author) { - $book->setAuthor($author, $author); - } + $book->setAuthor($this->author, $this->author); - $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->setSourceURL("http://$_SERVER[HTTP_HOST]"); + // I hope this is a non existant address :) + $book->setPublisher('wallabag', 'wallabag'); + // Strictly not needed as the book date defaults to time(). + $book->setDate(time()); + $book->setSourceURL($this->wallabagUrl); $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'PHP'); $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'wallabag'); @@ -129,31 +168,71 @@ class EntriesExport /* * Front page */ + if (file_exists($this->logoPath)) { + $book->setCoverImage('Cover.png', file_get_contents($this->logoPath), 'image/png'); + } - $book->setCoverImage('Cover.png', file_get_contents('themes/_global/img/appicon/apple-touch-icon-152.png'), 'image/png'); - - $cover = $content_start.'

'._('Produced by wallabag with PHPePub').'

'._('Please open an issue if you have trouble with the display of this E-Book on your device.').'

'.$bookEnd; - - $book->addChapter('Notices', 'Cover2.html', $cover); - - $book->buildTOC(); + $entryIds = []; + $entryCount = \count($this->entries); + $i = 0; /* * Adding actual entries */ - foreach ($this->entries as $entry) { //set tags as subjects - foreach ($this->tags as $tag) { - $book->setSubject($tag['value']); - } + // set tags as subjects + foreach ($this->entries as $entry) { + ++$i; + foreach ($entry->getTags() as $tag) { + $book->setSubject($tag->getLabel()); + } + $filename = sha1($entry->getTitle()); + + $publishedBy = $entry->getPublishedBy(); + $authors = $this->translator->trans('export.unknown'); + if (!empty($publishedBy)) { + $authors = implode(',', $publishedBy); + } - $chapter = $content_start.$entry->getContent().$bookEnd; - $book->addChapter($entry->getTitle(), htmlspecialchars($entry->getTitle()).'.html', $chapter, true, EPub::EXTERNAL_REF_ADD); + $titlepage = $content_start . + '

' . $entry->getTitle() . '

' . + '
' . + '
' . $this->translator->trans('entry.view.published_by') . '
' . $authors . '
' . + '
' . $this->translator->trans('entry.metadata.reading_time') . '
' . $this->translator->trans('entry.metadata.reading_time_minutes_short', ['%readingTime%' => $entry->getReadingTime()]) . '
' . + '
' . $this->translator->trans('entry.metadata.added_on') . '
' . $entry->getCreatedAt()->format('Y-m-d') . '
' . + '
' . $this->translator->trans('entry.metadata.address') . '
' . $entry->getUrl() . '
' . + '
' . + $bookEnd; + $book->addChapter("Entry {$i} of {$entryCount}", "{$filename}_cover.html", $titlepage, true, EPub::EXTERNAL_REF_ADD); + $chapter = $content_start . $entry->getContent() . $bookEnd; + + $entryIds[] = $entry->getId(); + $book->addChapter($entry->getTitle(), "{$filename}.html", $chapter, true, EPub::EXTERNAL_REF_ADD); } - $book->finalize(); - $book->sendBook($this->title); + + $book->addChapter('Notices', 'Cover2.html', $content_start . $this->getExportInformation('PHPePub') . $bookEnd); + + // Could also be the ISBN number, prefered for published books, or a UUID. + $hash = sha1(sprintf('%s:%s', $this->wallabagUrl, implode(',', $entryIds))); + $book->setIdentifier(sprintf('urn:wallabag:%s', $hash), EPub::IDENTIFIER_URI); + + return Response::create( + $book->getBook(), + 200, + [ + 'Content-Description' => 'File Transfer', + 'Content-type' => 'application/epub+zip', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.epub"', + 'Content-Transfer-Encoding' => 'binary', + ] + ); } + /** + * Use PHPMobi to dump a .mobi file. + * + * @return Response + */ private function produceMobi() { $mobi = new \MOBI(); @@ -162,23 +241,22 @@ class EntriesExport /* * Book metadata */ - $content->set('title', $this->title); - $content->set('author', implode($this->authors)); + $content->set('author', $this->author); $content->set('subject', $this->title); /* * Front page */ - - $content->appendParagraph('

'._('Produced by wallabag with PHPMobi').'

'._('Please open an issue if you have trouble with the display of this E-Book on your device.').'

'); - $content->appendImage(imagecreatefrompng('themes/_global/img/appicon/apple-touch-icon-152.png')); + $content->appendParagraph($this->getExportInformation('PHPMobi')); + if (file_exists($this->logoPath)) { + $content->appendImage(imagecreatefrompng($this->logoPath)); + } $content->appendPageBreak(); /* * Adding actual entries */ - foreach ($this->entries as $entry) { $content->appendChapterTitle($entry->getTitle()); $content->appendParagraph($entry->getContent()); @@ -186,78 +264,245 @@ class EntriesExport } $mobi->setContentProvider($content); - // the browser inside Kindle Devices doesn't likes special caracters either, we limit to A-z/0-9 - $this->title = preg_replace('/[^A-Za-z0-9\-]/', '', $this->title); - - // we offer file to download - $mobi->download($this->title.'.mobi'); + return Response::create( + $mobi->toString(), + 200, + [ + 'Accept-Ranges' => 'bytes', + 'Content-Description' => 'File Transfer', + 'Content-type' => 'application/x-mobipocket-ebook', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.mobi"', + 'Content-Transfer-Encoding' => 'binary', + ] + ); } - private function producePDF() + /** + * Use TCPDF to dump a .pdf file. + * + * @return Response + */ + private function producePdf() { $pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); /* * Book metadata */ - $pdf->SetCreator(PDF_CREATOR); - $pdf->SetAuthor('wallabag'); + $pdf->SetAuthor($this->author); $pdf->SetTitle($this->title); $pdf->SetSubject('Articles via wallabag'); $pdf->SetKeywords('wallabag'); - /* - * Front page - */ - - $pdf->AddPage(); - $intro = '

'.$this->title.'

-

'._('Produced by wallabag with tcpdf').'

-

'._('Please open an issue if you have trouble with the display of this E-Book on your device.').'

-
'; - - $pdf->writeHTMLCell(0, 0, '', '', $intro, 0, 1, 0, true, '', true); - /* * Adding actual entries */ - foreach ($this->entries as $entry) { - foreach ($this->tags as $tag) { - $pdf->SetKeywords($tag['value']); + foreach ($entry->getTags() as $tag) { + $pdf->SetKeywords($tag->getLabel()); } + $publishedBy = $entry->getPublishedBy(); + $authors = $this->translator->trans('export.unknown'); + if (!empty($publishedBy)) { + $authors = implode(',', $publishedBy); + } + + $pdf->addPage(); + $html = '

' . $entry->getTitle() . '

' . + '
' . + '
' . $this->translator->trans('entry.view.published_by') . '
' . $authors . '
' . + '
' . $this->translator->trans('entry.metadata.reading_time') . '
' . $this->translator->trans('entry.metadata.reading_time_minutes_short', ['%readingTime%' => $entry->getReadingTime()]) . '
' . + '
' . $this->translator->trans('entry.metadata.added_on') . '
' . $entry->getCreatedAt()->format('Y-m-d') . '
' . + '
' . $this->translator->trans('entry.metadata.address') . '
' . $entry->getUrl() . '
' . + '
'; + $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + $pdf->AddPage(); - $html = '

'.$entry->getTitle().'

'; + $html = '

' . $entry->getTitle() . '

'; $html .= $entry->getContent(); + $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); } + /* + * Last page + */ + $pdf->AddPage(); + $html = $this->getExportInformation('tcpdf'); + + $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); + // set image scale factor $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); - $pdf->Output($this->title.'.pdf', 'D'); + return Response::create( + $pdf->Output('', 'S'), + 200, + [ + 'Content-Description' => 'File Transfer', + 'Content-type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.pdf"', + 'Content-Transfer-Encoding' => 'binary', + ] + ); } - private function produceCSV() + /** + * Inspired from CsvFileDumper. + * + * @return Response + */ + private function produceCsv() { - header('Content-type: application/csv'); - header('Content-Disposition: attachment; filename="'.$this->title.'.csv"'); - header('Content-Transfer-Encoding: UTF-8'); + $delimiter = ';'; + $enclosure = '"'; + $handle = fopen('php://memory', 'b+r'); - $output = fopen('php://output', 'a'); + fputcsv($handle, ['Title', 'URL', 'Content', 'Tags', 'MIME Type', 'Language', 'Creation date'], $delimiter, $enclosure); - fputcsv($output, array('Title', 'URL', 'Content', 'Tags', 'MIME Type', 'Language')); foreach ($this->entries as $entry) { - fputcsv($output, array($entry->getTitle(), - $entry->getURL(), - $entry->getContent(), - implode(', ', $entry->getTags()->toArray()), - $entry->getMimetype(), - $entry->getLanguage(), )); + fputcsv( + $handle, + [ + $entry->getTitle(), + $entry->getURL(), + // remove new line to avoid crazy results + str_replace(["\r\n", "\r", "\n"], '', $entry->getContent()), + implode(', ', $entry->getTags()->toArray()), + $entry->getMimetype(), + $entry->getLanguage(), + $entry->getCreatedAt()->format('d/m/Y h:i:s'), + ], + $delimiter, + $enclosure + ); } - fclose($output); - exit(); + + rewind($handle); + $output = stream_get_contents($handle); + fclose($handle); + + return Response::create( + $output, + 200, + [ + 'Content-type' => 'application/csv', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.csv"', + 'Content-Transfer-Encoding' => 'UTF-8', + ] + ); + } + + /** + * Dump a JSON file. + * + * @return Response + */ + private function produceJson() + { + return Response::create( + $this->prepareSerializingContent('json'), + 200, + [ + 'Content-type' => 'application/json', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.json"', + 'Content-Transfer-Encoding' => 'UTF-8', + ] + ); + } + + /** + * Dump a XML file. + * + * @return Response + */ + private function produceXml() + { + return Response::create( + $this->prepareSerializingContent('xml'), + 200, + [ + 'Content-type' => 'application/xml', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.xml"', + 'Content-Transfer-Encoding' => 'UTF-8', + ] + ); + } + + /** + * Dump a TXT file. + * + * @return Response + */ + private function produceTxt() + { + $content = ''; + $bar = str_repeat('=', 100); + foreach ($this->entries as $entry) { + $content .= "\n\n" . $bar . "\n\n" . $entry->getTitle() . "\n\n" . $bar . "\n\n"; + $html = new Html2Text($entry->getContent(), ['do_links' => 'none', 'width' => 100]); + $content .= $html->getText(); + } + + return Response::create( + $content, + 200, + [ + 'Content-type' => 'text/plain', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.txt"', + 'Content-Transfer-Encoding' => 'UTF-8', + ] + ); + } + + /** + * Return a Serializer object for producing processes that need it (JSON & XML). + * + * @param string $format + * + * @return string + */ + private function prepareSerializingContent($format) + { + $serializer = SerializerBuilder::create()->build(); + + return $serializer->serialize( + $this->entries, + $format, + SerializationContext::create()->setGroups(['entries_for_user']) + ); + } + + /** + * Return a kind of footer / information for the epub. + * + * @param string $type Generator of the export, can be: tdpdf, PHPePub, PHPMobi + * + * @return string + */ + private function getExportInformation($type) + { + $info = $this->translator->trans('export.footer_template', [ + '%method%' => $type, + ]); + + if ('tcpdf' === $type) { + return str_replace('%IMAGE%', '', $info); + } + + return str_replace('%IMAGE%', '', $info); + } + + /** + * Return a sanitized version of the title by applying translit iconv + * and removing non alphanumeric characters, - and space. + * + * @return string Sanitized filename + */ + private function getSanitizedFilename() + { + return preg_replace('/[^A-Za-z0-9\- \']/', '', iconv('utf-8', 'us-ascii//TRANSLIT', $this->title)); } }