diff options
Diffstat (limited to 'src/Wallabag/CoreBundle/Helper/EntriesExport.php')
-rw-r--r-- | src/Wallabag/CoreBundle/Helper/EntriesExport.php | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/src/Wallabag/CoreBundle/Helper/EntriesExport.php b/src/Wallabag/CoreBundle/Helper/EntriesExport.php new file mode 100644 index 00000000..d6a4d094 --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/EntriesExport.php | |||
@@ -0,0 +1,394 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Wallabag\CoreBundle\Helper; | ||
4 | |||
5 | use PHPePub\Core\EPub; | ||
6 | use PHPePub\Core\Structure\OPF\DublinCore; | ||
7 | use Symfony\Component\HttpFoundation\Response; | ||
8 | use JMS\Serializer; | ||
9 | use JMS\Serializer\SerializerBuilder; | ||
10 | use JMS\Serializer\SerializationContext; | ||
11 | |||
12 | /** | ||
13 | * This class doesn't have unit test BUT it's fully covered by a functional test with ExportControllerTest. | ||
14 | */ | ||
15 | class EntriesExport | ||
16 | { | ||
17 | private $wallabagUrl; | ||
18 | private $logoPath; | ||
19 | private $title = ''; | ||
20 | private $entries = array(); | ||
21 | private $authors = array('wallabag'); | ||
22 | private $language = ''; | ||
23 | private $tags = array(); | ||
24 | private $footerTemplate = '<div style="text-align:center;"> | ||
25 | <p>Produced by wallabag with %EXPORT_METHOD%</p> | ||
26 | <p>Please open <a href="https://github.com/wallabag/wallabag/issues">an issue</a> if you have trouble with the display of this E-Book on your device.</p> | ||
27 | </div'; | ||
28 | |||
29 | /** | ||
30 | * @param string $wallabagUrl Wallabag instance url | ||
31 | * @param string $logoPath Path to the logo FROM THE BUNDLE SCOPE | ||
32 | */ | ||
33 | public function __construct($wallabagUrl, $logoPath) | ||
34 | { | ||
35 | $this->wallabagUrl = $wallabagUrl; | ||
36 | $this->logoPath = $logoPath; | ||
37 | } | ||
38 | |||
39 | /** | ||
40 | * Define entries. | ||
41 | * | ||
42 | * @param array|Entry $entries An array of entries or one entry | ||
43 | */ | ||
44 | public function setEntries($entries) | ||
45 | { | ||
46 | if (!is_array($entries)) { | ||
47 | $this->language = $entries->getLanguage(); | ||
48 | $entries = array($entries); | ||
49 | } | ||
50 | |||
51 | $this->entries = $entries; | ||
52 | |||
53 | foreach ($entries as $entry) { | ||
54 | $this->tags[] = $entry->getTags(); | ||
55 | } | ||
56 | |||
57 | return $this; | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * Sets the category of which we want to get articles, or just one entry. | ||
62 | * | ||
63 | * @param string $method Method to get articles | ||
64 | */ | ||
65 | public function updateTitle($method) | ||
66 | { | ||
67 | $this->title = $method.' articles'; | ||
68 | |||
69 | if ('entry' === $method) { | ||
70 | $this->title = $this->entries[0]->getTitle(); | ||
71 | } | ||
72 | |||
73 | return $this; | ||
74 | } | ||
75 | |||
76 | /** | ||
77 | * Sets the output format. | ||
78 | * | ||
79 | * @param string $format | ||
80 | */ | ||
81 | public function exportAs($format) | ||
82 | { | ||
83 | switch ($format) { | ||
84 | case 'epub': | ||
85 | return $this->produceEpub(); | ||
86 | |||
87 | case 'mobi': | ||
88 | return $this->produceMobi(); | ||
89 | |||
90 | case 'pdf': | ||
91 | return $this->producePDF(); | ||
92 | |||
93 | case 'csv': | ||
94 | return $this->produceCSV(); | ||
95 | |||
96 | case 'json': | ||
97 | return $this->produceJSON(); | ||
98 | |||
99 | case 'xml': | ||
100 | return $this->produceXML(); | ||
101 | } | ||
102 | |||
103 | throw new \InvalidArgumentException(sprintf('The format "%s" is not yet supported.', $format)); | ||
104 | } | ||
105 | |||
106 | /** | ||
107 | * Use PHPePub to dump a .epub file. | ||
108 | */ | ||
109 | private function produceEpub() | ||
110 | { | ||
111 | /* | ||
112 | * Start and End of the book | ||
113 | */ | ||
114 | $content_start = | ||
115 | "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" | ||
116 | ."<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n" | ||
117 | .'<head>' | ||
118 | ."<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n" | ||
119 | ."<title>wallabag articles book</title>\n" | ||
120 | ."</head>\n" | ||
121 | ."<body>\n"; | ||
122 | |||
123 | $bookEnd = "</body>\n</html>\n"; | ||
124 | |||
125 | $book = new EPub(EPub::BOOK_VERSION_EPUB3); | ||
126 | |||
127 | /* | ||
128 | * Book metadata | ||
129 | */ | ||
130 | |||
131 | $book->setTitle($this->title); | ||
132 | // Could also be the ISBN number, prefered for published books, or a UUID. | ||
133 | $book->setIdentifier($this->title, EPub::IDENTIFIER_URI); | ||
134 | // 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. | ||
135 | $book->setLanguage($this->language); | ||
136 | $book->setDescription('Some articles saved on my wallabag'); | ||
137 | |||
138 | foreach ($this->authors as $author) { | ||
139 | $book->setAuthor($author, $author); | ||
140 | } | ||
141 | |||
142 | // I hope this is a non existant address :) | ||
143 | $book->setPublisher('wallabag', 'wallabag'); | ||
144 | // Strictly not needed as the book date defaults to time(). | ||
145 | $book->setDate(time()); | ||
146 | $book->setSourceURL($this->wallabagUrl); | ||
147 | |||
148 | $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'PHP'); | ||
149 | $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'wallabag'); | ||
150 | |||
151 | /* | ||
152 | * Front page | ||
153 | */ | ||
154 | if (file_exists($this->logoPath)) { | ||
155 | $book->setCoverImage('Cover.png', file_get_contents($this->logoPath), 'image/png'); | ||
156 | } | ||
157 | |||
158 | $book->addChapter('Notices', 'Cover2.html', $content_start.$this->getExportInformation('PHPePub').$bookEnd); | ||
159 | |||
160 | $book->buildTOC(); | ||
161 | |||
162 | /* | ||
163 | * Adding actual entries | ||
164 | */ | ||
165 | |||
166 | // set tags as subjects | ||
167 | foreach ($this->entries as $entry) { | ||
168 | foreach ($this->tags as $tag) { | ||
169 | $book->setSubject($tag['value']); | ||
170 | } | ||
171 | |||
172 | $chapter = $content_start.$entry->getContent().$bookEnd; | ||
173 | $book->addChapter($entry->getTitle(), htmlspecialchars($entry->getTitle()).'.html', $chapter, true, EPub::EXTERNAL_REF_ADD); | ||
174 | } | ||
175 | |||
176 | return Response::create( | ||
177 | $book->getBook(), | ||
178 | 200, | ||
179 | array( | ||
180 | 'Content-Description' => 'File Transfer', | ||
181 | 'Content-type' => 'application/epub+zip', | ||
182 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.epub"', | ||
183 | 'Content-Transfer-Encoding' => 'binary', | ||
184 | ) | ||
185 | )->send(); | ||
186 | } | ||
187 | |||
188 | /** | ||
189 | * Use PHPMobi to dump a .mobi file. | ||
190 | */ | ||
191 | private function produceMobi() | ||
192 | { | ||
193 | $mobi = new \MOBI(); | ||
194 | $content = new \MOBIFile(); | ||
195 | |||
196 | /* | ||
197 | * Book metadata | ||
198 | */ | ||
199 | $content->set('title', $this->title); | ||
200 | $content->set('author', implode($this->authors)); | ||
201 | $content->set('subject', $this->title); | ||
202 | |||
203 | /* | ||
204 | * Front page | ||
205 | */ | ||
206 | $content->appendParagraph($this->getExportInformation('PHPMobi')); | ||
207 | if (file_exists($this->logoPath)) { | ||
208 | $content->appendImage(imagecreatefrompng($this->logoPath)); | ||
209 | } | ||
210 | $content->appendPageBreak(); | ||
211 | |||
212 | /* | ||
213 | * Adding actual entries | ||
214 | */ | ||
215 | foreach ($this->entries as $entry) { | ||
216 | $content->appendChapterTitle($entry->getTitle()); | ||
217 | $content->appendParagraph($entry->getContent()); | ||
218 | $content->appendPageBreak(); | ||
219 | } | ||
220 | $mobi->setContentProvider($content); | ||
221 | |||
222 | // the browser inside Kindle Devices doesn't likes special caracters either, we limit to A-z/0-9 | ||
223 | $this->title = preg_replace('/[^A-Za-z0-9\-]/', '', $this->title); | ||
224 | |||
225 | return Response::create( | ||
226 | $mobi->toString(), | ||
227 | 200, | ||
228 | array( | ||
229 | 'Accept-Ranges' => 'bytes', | ||
230 | 'Content-Description' => 'File Transfer', | ||
231 | 'Content-type' => 'application/x-mobipocket-ebook', | ||
232 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.mobi"', | ||
233 | 'Content-Transfer-Encoding' => 'binary', | ||
234 | ) | ||
235 | )->send(); | ||
236 | } | ||
237 | |||
238 | /** | ||
239 | * Use TCPDF to dump a .pdf file. | ||
240 | */ | ||
241 | private function producePDF() | ||
242 | { | ||
243 | $pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); | ||
244 | |||
245 | /* | ||
246 | * Book metadata | ||
247 | */ | ||
248 | $pdf->SetCreator(PDF_CREATOR); | ||
249 | $pdf->SetAuthor('wallabag'); | ||
250 | $pdf->SetTitle($this->title); | ||
251 | $pdf->SetSubject('Articles via wallabag'); | ||
252 | $pdf->SetKeywords('wallabag'); | ||
253 | |||
254 | /* | ||
255 | * Front page | ||
256 | */ | ||
257 | $pdf->AddPage(); | ||
258 | $intro = '<h1>'.$this->title.'</h1>'.$this->getExportInformation('tcpdf'); | ||
259 | |||
260 | $pdf->writeHTMLCell(0, 0, '', '', $intro, 0, 1, 0, true, '', true); | ||
261 | |||
262 | /* | ||
263 | * Adding actual entries | ||
264 | */ | ||
265 | foreach ($this->entries as $entry) { | ||
266 | foreach ($this->tags as $tag) { | ||
267 | $pdf->SetKeywords($tag['value']); | ||
268 | } | ||
269 | |||
270 | $pdf->AddPage(); | ||
271 | $html = '<h1>'.$entry->getTitle().'</h1>'; | ||
272 | $html .= $entry->getContent(); | ||
273 | |||
274 | $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); | ||
275 | } | ||
276 | |||
277 | // set image scale factor | ||
278 | $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); | ||
279 | |||
280 | return Response::create( | ||
281 | $pdf->Output('', 'S'), | ||
282 | 200, | ||
283 | array( | ||
284 | 'Content-Description' => 'File Transfer', | ||
285 | 'Content-type' => 'application/pdf', | ||
286 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.pdf"', | ||
287 | 'Content-Transfer-Encoding' => 'binary', | ||
288 | ) | ||
289 | )->send(); | ||
290 | } | ||
291 | |||
292 | /** | ||
293 | * Inspired from CsvFileDumper. | ||
294 | */ | ||
295 | private function produceCSV() | ||
296 | { | ||
297 | $delimiter = ';'; | ||
298 | $enclosure = '"'; | ||
299 | $handle = fopen('php://memory', 'rb+'); | ||
300 | |||
301 | fputcsv($handle, array('Title', 'URL', 'Content', 'Tags', 'MIME Type', 'Language'), $delimiter, $enclosure); | ||
302 | |||
303 | foreach ($this->entries as $entry) { | ||
304 | fputcsv( | ||
305 | $handle, | ||
306 | array( | ||
307 | $entry->getTitle(), | ||
308 | $entry->getURL(), | ||
309 | // remove new line to avoid crazy results | ||
310 | str_replace(array("\r\n", "\r", "\n"), '', $entry->getContent()), | ||
311 | implode(', ', $entry->getTags()->toArray()), | ||
312 | $entry->getMimetype(), | ||
313 | $entry->getLanguage(), | ||
314 | ), | ||
315 | $delimiter, | ||
316 | $enclosure | ||
317 | ); | ||
318 | } | ||
319 | |||
320 | rewind($handle); | ||
321 | $output = stream_get_contents($handle); | ||
322 | fclose($handle); | ||
323 | |||
324 | return Response::create( | ||
325 | $output, | ||
326 | 200, | ||
327 | array( | ||
328 | 'Content-type' => 'application/csv', | ||
329 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.csv"', | ||
330 | 'Content-Transfer-Encoding' => 'UTF-8', | ||
331 | ) | ||
332 | )->send(); | ||
333 | } | ||
334 | |||
335 | private function produceJSON() | ||
336 | { | ||
337 | return Response::create( | ||
338 | $this->prepareSerializingContent('json'), | ||
339 | 200, | ||
340 | array( | ||
341 | 'Content-type' => 'application/json', | ||
342 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.json"', | ||
343 | 'Content-Transfer-Encoding' => 'UTF-8', | ||
344 | ) | ||
345 | )->send(); | ||
346 | } | ||
347 | |||
348 | private function produceXML() | ||
349 | { | ||
350 | return Response::create( | ||
351 | $this->prepareSerializingContent('xml'), | ||
352 | 200, | ||
353 | array( | ||
354 | 'Content-type' => 'application/xml', | ||
355 | 'Content-Disposition' => 'attachment; filename="'.$this->title.'.xml"', | ||
356 | 'Content-Transfer-Encoding' => 'UTF-8', | ||
357 | ) | ||
358 | )->send(); | ||
359 | } | ||
360 | |||
361 | /** | ||
362 | * Return a Serializer object for producing processes that need it (JSON & XML). | ||
363 | * | ||
364 | * @return Serializer | ||
365 | */ | ||
366 | private function prepareSerializingContent($format) | ||
367 | { | ||
368 | $serializer = SerializerBuilder::create()->build(); | ||
369 | |||
370 | return $serializer->serialize( | ||
371 | $this->entries, | ||
372 | $format, | ||
373 | SerializationContext::create()->setGroups(array('entries_for_user')) | ||
374 | ); | ||
375 | } | ||
376 | |||
377 | /** | ||
378 | * Return a kind of footer / information for the epub. | ||
379 | * | ||
380 | * @param string $type Generator of the export, can be: tdpdf, PHPePub, PHPMobi | ||
381 | * | ||
382 | * @return string | ||
383 | */ | ||
384 | private function getExportInformation($type) | ||
385 | { | ||
386 | $info = str_replace('%EXPORT_METHOD%', $type, $this->footerTemplate); | ||
387 | |||
388 | if ('tcpdf' === $type) { | ||
389 | return str_replace('%IMAGE%', '<img src="'.$this->logoPath.'" />', $info); | ||
390 | } | ||
391 | |||
392 | return str_replace('%IMAGE%', '', $info); | ||
393 | } | ||
394 | } | ||