From: Jeremy Benoist Date: Fri, 16 Oct 2015 08:51:53 +0000 (+0200) Subject: Rework on export X-Git-Tag: 2.0.0-alpha.1~14^2~8 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=add597bad95b30dbecab3aecc8362a1ccd427976;p=github%2Fwallabag%2Fwallabag.git Rework on export - all export now return a `HttpFoundation\Response` - return a 404 on unsupported format - add tests --- diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index 52f9bccb..b475d637 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -51,6 +51,7 @@ parameters: export_epub: true export_mobi: true export_pdf: true + wallabag_url: http://v2.wallabag.org # default user config items_on_page: 12 diff --git a/app/config/tests/parameters.yml.dist.mysql b/app/config/tests/parameters.yml.dist.mysql index 03fdf5a6..5b29690c 100644 --- a/app/config/tests/parameters.yml.dist.mysql +++ b/app/config/tests/parameters.yml.dist.mysql @@ -51,6 +51,7 @@ parameters: export_epub: true export_mobi: true export_pdf: true + wallabag_url: http://v2.wallabag.org # default user config items_on_page: 12 diff --git a/app/config/tests/parameters.yml.dist.pgsql b/app/config/tests/parameters.yml.dist.pgsql index 675ba6c9..efdac961 100644 --- a/app/config/tests/parameters.yml.dist.pgsql +++ b/app/config/tests/parameters.yml.dist.pgsql @@ -51,6 +51,7 @@ parameters: export_epub: true export_mobi: true export_pdf: true + wallabag_url: http://v2.wallabag.org # default user config items_on_page: 12 diff --git a/app/config/tests/parameters.yml.dist.sqlite b/app/config/tests/parameters.yml.dist.sqlite index 258627af..276d1147 100644 --- a/app/config/tests/parameters.yml.dist.sqlite +++ b/app/config/tests/parameters.yml.dist.sqlite @@ -51,6 +51,7 @@ parameters: export_epub: true export_mobi: true export_pdf: true + wallabag_url: http://v2.wallabag.org # default user config items_on_page: 12 diff --git a/composer.json b/composer.json index a25e143a..b6a9c854 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "friendsofsymfony/oauth-server-bundle": "^1.4@dev", "scheb/two-factor-bundle": "~1.4", "grandt/phpepub": "~4.0", - "wallabag/phpMobi": "~1.0.0" + "wallabag/php-mobi": "~1.0.0" }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "~2.2.0", diff --git a/composer.lock b/composer.lock index c634abbd..ae53a6d6 100644 --- a/composer.lock +++ b/composer.lock @@ -3796,41 +3796,52 @@ "time": "2015-11-05 12:49:06" }, { - "name": "wallabag/phpMobi", - "version": "1.0.0", + "name": "wallabag/php-mobi", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/wallabag/phpMobi.git", - "reference": "5137696542f08f8e6a0603c01970c6d3eca9873d" + "reference": "1cd7d022fe6be838535d6bba917d19cc48dcf487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wallabag/phpMobi/zipball/5137696542f08f8e6a0603c01970c6d3eca9873d", - "reference": "5137696542f08f8e6a0603c01970c6d3eca9873d", + "url": "https://api.github.com/repos/wallabag/phpMobi/zipball/1cd7d022fe6be838535d6bba917d19cc48dcf487", + "reference": "1cd7d022fe6be838535d6bba917d19cc48dcf487", "shasum": "" }, "require": { "php": ">=5.3.0" }, + "replace": { + "wallabag/phpmobi": "*" + }, "type": "library", "autoload": { "files": [ "MOBIClass/MOBI.php" ] }, + "license": [ + "Apache-2.0" + ], "authors": [ + { + "name": "Sander Kromwijk", + "email": "s.kromwijk@gmail.co", + "role": "Original developer" + }, { "name": "Nicolas Lœuillet", "email": "nicolas@loeuillet.org", "homepage": "http://www.cdetc.fr" } ], - "description": "An experimental Mobipocket file creator in PHP.", + "description": "A Mobipocket file (.mobi) creator in PHP.", "homepage": "https://github.com/wallabag/phpMobi", "support": { - "source": "https://github.com/wallabag/phpMobi/tree/1.0.0" + "source": "https://github.com/wallabag/phpMobi/tree/1.0.1" }, - "time": "2015-01-19 12:43:17" + "time": "2015-10-16 08:42:42" }, { "name": "willdurand/hateoas", diff --git a/src/Wallabag/CoreBundle/Controller/ExportController.php b/src/Wallabag/CoreBundle/Controller/ExportController.php index 123e491a..dd3cb7ca 100644 --- a/src/Wallabag/CoreBundle/Controller/ExportController.php +++ b/src/Wallabag/CoreBundle/Controller/ExportController.php @@ -4,62 +4,55 @@ namespace Wallabag\CoreBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Wallabag\CoreBundle\Entity\Entry; -use Wallabag\CoreBundle\Helper\EntriesExport; class ExportController extends Controller { /** - * Gets all entries for current user. + * Gets one entry content. * - * @Route("/export/{category}.{format}", name="ebook", requirements={ - * "_format": "epub|mobi|pdf|json|xml|txt|csv" - * }) + * @param Entry $entry + * + * @Route("/export/{id}.{format}", requirements={"id" = "\d+"}, name="export_entry") */ - public function getEntriesAction($format, $category) + public function downloadEntryAction(Entry $entry, $format) { - $repository = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry'); - switch ($category) { - case 'all': - $method = 'All'; - break; - - case 'unread': - $method = 'Unread'; - break; - - case 'starred': - $method = 'Starred'; - break; - - case 'archive': - $method = 'Archive'; - break; - - default: - break; + try { + return $this->get('wallabag_core.helper.entries_export') + ->setEntries($entry) + ->updateTitle('entry') + ->exportAs($format); + } catch (\InvalidArgumentException $e) { + throw new NotFoundHttpException($e->getMessage()); } - - $methodBuilder = 'getBuilderFor'.$method.'ByUser'; - $qb = $repository->$methodBuilder($this->getUser()->getId()); - $entries = $qb->getQuery()->getResult(); - - $export = new EntriesExport($entries); - $export->setMethod($method); - $export->exportAs($format); } /** - * Gets one entry content. - * - * @param Entry $entry + * Export all entries for current user. * - * @Route("/export/id/{id}.{format}", requirements={"id" = "\d+"}, name="ebook_entry") + * @Route("/export/{category}.{format}", name="export_entries", requirements={ + * "_format": "epub|mobi|pdf|json|xml|txt|csv", + * "category": "all|unread|starred|archive" + * }) */ - public function getEntryAction(Entry $entry, $format) + public function downloadEntriesAction($format, $category) { - $export = new EntriesExport(array($entry)); - $export->setMethod('entry'); - $export->exportAs($format); + $method = ucfirst($category); + $methodBuilder = 'getBuilderFor'.$method.'ByUser'; + $entries = $this->getDoctrine() + ->getRepository('WallabagCoreBundle:Entry') + ->$methodBuilder($this->getUser()->getId()) + ->getQuery() + ->getResult(); + + try { + return $this->get('wallabag_core.helper.entries_export') + ->setEntries($entries) + ->updateTitle($method) + ->exportAs($format); + } catch (\InvalidArgumentException $e) { + throw new NotFoundHttpException($e->getMessage()); + } } } diff --git a/src/Wallabag/CoreBundle/Helper/EntriesExport.php b/src/Wallabag/CoreBundle/Helper/EntriesExport.php index fad0bb97..806319b1 100644 --- a/src/Wallabag/CoreBundle/Helper/EntriesExport.php +++ b/src/Wallabag/CoreBundle/Helper/EntriesExport.php @@ -4,27 +4,51 @@ namespace Wallabag\CoreBundle\Helper; use PHPePub\Core\EPub; use PHPePub\Core\Structure\OPF\DublinCore; +use Symfony\Component\HttpFoundation\Response; class EntriesExport { - private $format; - private $method; - private $title; - private $entries; + private $wallabagUrl; + private $logoPath; + private $title = ''; + private $entries = array(); private $authors = array('wallabag'); - private $language; - private $tags; + private $language = ''; + private $tags = array(); + private $footerTemplate = '
+

Produced by wallabag with %EXPORT_METHOD%

+

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

+ wallabagUrl = $wallabagUrl; + $this->logoPath = $logoPath; + } + + /** + * Define entries. + * + * @param array|Entry $entries An array of entries or one entry + */ + public function setEntries($entries) + { + if (!is_array($entries)) { + $this->language = $entries->getLanguage(); + $entries = array($entries); + } + $this->entries = $entries; foreach ($entries as $entry) { $this->tags[] = $entry->getTags(); } - if (count($entries) === 1) { - $this->language = $entries[0]->getLanguage(); - } + + return $this; } /** @@ -32,29 +56,15 @@ class EntriesExport * * @param string $method Method to get articles */ - 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; } /** @@ -64,30 +74,26 @@ class EntriesExport */ public function exportAs($format) { - $this->format = $format; - - switch ($this->format) { + switch ($format) { case 'epub': - $this->produceEpub(); - break; + return $this->produceEpub(); case 'mobi': - $this->produceMobi(); - break; + return $this->produceMobi(); case 'pdf': - $this->producePDF(); - break; + return $this->producePDF(); case 'csv': - $this->produceCSV(); - break; - - default: - break; + return $this->produceCSV(); } + + throw new \InvalidArgumentException(sprintf('The format "%s" is not yet supported.', $format)); } + /** + * Use PHPePub to dump a .epub file. + */ private function produceEpub() { /* @@ -98,7 +104,7 @@ class EntriesExport ."\n" .'' ."\n" - .''._('wallabag articles book')."\n" + ."wallabag articles book\n" ."\n" ."\n"; @@ -111,17 +117,21 @@ 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')); + // Could also be the ISBN number, prefered for published books, or a UUID. + $book->setIdentifier($this->title, EPub::IDENTIFIER_URI); + // 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->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,12 +139,11 @@ 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->addChapter('Notices', 'Cover2.html', $content_start.$this->getExportInformation('PHPePub').$bookEnd); $book->buildTOC(); @@ -142,18 +151,31 @@ class EntriesExport * 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) { + foreach ($this->tags as $tag) { + $book->setSubject($tag['value']); + } $chapter = $content_start.$entry->getContent().$bookEnd; $book->addChapter($entry->getTitle(), htmlspecialchars($entry->getTitle()).'.html', $chapter, true, EPub::EXTERNAL_REF_ADD); } - $book->finalize(); - $book->sendBook($this->title); + + return Response::create( + $book->getBook(), + 200, + array( + 'Content-Description' => 'File Transfer', + 'Content-type' => 'application/epub+zip', + 'Content-Disposition' => 'attachment; filename="'.$this->title.'.epub"', + 'Content-Transfer-Encoding' => 'binary', + ) + )->send(); } + /** + * Use PHPMobi to dump a .mobi file. + */ private function produceMobi() { $mobi = new \MOBI(); @@ -162,7 +184,6 @@ class EntriesExport /* * Book metadata */ - $content->set('title', $this->title); $content->set('author', implode($this->authors)); $content->set('subject', $this->title); @@ -170,15 +191,15 @@ class EntriesExport /* * 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()); @@ -189,10 +210,22 @@ class EntriesExport // 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, + array( + 'Accept-Ranges' => 'bytes', + 'Content-Description' => 'File Transfer', + 'Content-type' => 'application/x-mobipocket-ebook', + 'Content-Disposition' => 'attachment; filename="'.$this->title.'.mobi"', + 'Content-Transfer-Encoding' => 'binary', + ) + )->send(); } + /** + * Use TCPDF to dump a .pdf file. + */ private function producePDF() { $pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); @@ -200,7 +233,6 @@ class EntriesExport /* * Book metadata */ - $pdf->SetCreator(PDF_CREATOR); $pdf->SetAuthor('wallabag'); $pdf->SetTitle($this->title); @@ -210,19 +242,14 @@ class EntriesExport /* * 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.').'

-
'; + $intro = '

'.$this->title.'

'.$this->getExportInformation('tcpdf'); $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']); @@ -231,33 +258,82 @@ class EntriesExport $pdf->AddPage(); $html = '

'.$entry->getTitle().'

'; $html .= $entry->getContent(); + $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, + array( + 'Content-Description' => 'File Transfer', + 'Content-type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="'.$this->title.'.pdf"', + 'Content-Transfer-Encoding' => 'binary', + ) + )->send(); } + /** + * Inspired from CsvFileDumper. + */ 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', 'rb+'); - $output = fopen('php://output', 'a'); + fputcsv($handle, array('Title', 'URL', 'Content', 'Tags', 'MIME Type', 'Language'), $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, + array( + $entry->getTitle(), + $entry->getURL(), + $entry->getContent(), + implode(', ', $entry->getTags()->toArray()), + $entry->getMimetype(), + $entry->getLanguage(), + ), + $delimiter, + $enclosure + ); + } + + rewind($handle); + $output = stream_get_contents($handle); + fclose($handle); + + return Response::create( + $output, + 200, + array( + 'Content-type' => 'application/csv', + 'Content-Disposition' => 'attachment; filename="'.$this->title.'.csv"', + 'Content-Transfer-Encoding' => 'UTF-8', + ) + )->send(); + } + + /** + * 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 = str_replace('%EXPORT_METHOD%', $type, $this->footerTemplate); + + if ('tcpdf' === $type) { + return str_replace('%IMAGE%', '', $info); } - fclose($output); - exit(); + + return str_replace('%IMAGE%', '', $info); } } diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml index 65c2c8d8..8e21b052 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.yml +++ b/src/Wallabag/CoreBundle/Resources/config/services.yml @@ -64,3 +64,9 @@ services: - %language% tags: - { name: kernel.event_subscriber } + + wallabag_core.helper.entries_export: + class: Wallabag\CoreBundle\Helper\EntriesExport + arguments: + - %wallabag_url% + - src/Wallabag/CoreBundle/Resources/views/themes/_global/public/img/appicon/apple-touch-icon-152.png diff --git a/src/Wallabag/CoreBundle/Tests/Controller/ExportControllerTest.php b/src/Wallabag/CoreBundle/Tests/Controller/ExportControllerTest.php new file mode 100644 index 00000000..19d5f01d --- /dev/null +++ b/src/Wallabag/CoreBundle/Tests/Controller/ExportControllerTest.php @@ -0,0 +1,116 @@ +getClient(); + + $client->request('GET', '/export/unread.csv'); + + $this->assertEquals(302, $client->getResponse()->getStatusCode()); + $this->assertContains('login', $client->getResponse()->headers->get('location')); + } + + public function testUnknownCategoryExport() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/export/awesomeness.epub'); + + $this->assertEquals(404, $client->getResponse()->getStatusCode()); + } + + public function testUnknownFormatExport() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $crawler = $client->request('GET', '/export/unread.xslx'); + + $this->assertEquals(404, $client->getResponse()->getStatusCode()); + } + + public function testEpubExport() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + ob_start(); + $crawler = $client->request('GET', '/export/archive.epub'); + ob_end_clean(); + + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + $headers = $client->getResponse()->headers; + $this->assertEquals('application/epub+zip', $headers->get('content-type')); + $this->assertEquals('attachment; filename="Archive articles.epub"', $headers->get('content-disposition')); + $this->assertEquals('binary', $headers->get('content-transfer-encoding')); + } + + public function testMobiExport() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + $content = $client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Entry') + ->findOneByUsernameAndNotArchived('admin'); + + ob_start(); + $crawler = $client->request('GET', '/export/'.$content->getId().'.mobi'); + ob_end_clean(); + + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + $headers = $client->getResponse()->headers; + $this->assertEquals('application/x-mobipocket-ebook', $headers->get('content-type')); + $this->assertEquals('attachment; filename="testtitleentry1.mobi"', $headers->get('content-disposition')); + $this->assertEquals('binary', $headers->get('content-transfer-encoding')); + } + + public function testPdfExport() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + ob_start(); + $crawler = $client->request('GET', '/export/all.pdf'); + ob_end_clean(); + + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + $headers = $client->getResponse()->headers; + $this->assertEquals('application/pdf', $headers->get('content-type')); + $this->assertEquals('attachment; filename="All articles.pdf"', $headers->get('content-disposition')); + $this->assertEquals('binary', $headers->get('content-transfer-encoding')); + } + + public function testCsvExport() + { + $this->logInAs('admin'); + $client = $this->getClient(); + + ob_start(); + $crawler = $client->request('GET', '/export/unread.csv'); + ob_end_clean(); + + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + $headers = $client->getResponse()->headers; + $this->assertEquals('application/csv', $headers->get('content-type')); + $this->assertEquals('attachment; filename="Unread articles.csv"', $headers->get('content-disposition')); + $this->assertEquals('UTF-8', $headers->get('content-transfer-encoding')); + + $csv = str_getcsv($client->getResponse()->getContent(), "\n"); + + $this->assertGreaterThan(1, $csv); + $this->assertEquals('Title;URL;Content;Tags;"MIME Type";Language', $csv[0]); + } +}