EntriesExport   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 512
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 38
eloc 219
dl 0
loc 512
rs 9.36
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A setEntries() 0 10 2
A exportJsonData() 0 3 1
A produceXml() 0 9 1
A produceTxt() 0 17 2
A getExportInformation() 0 11 2
A updateTitle() 0 9 2
A getSanitizedFilename() 0 3 1
A exportAs() 0 8 2
B produceEpub() 0 108 7
A prepareSerializingContent() 0 8 1
A produceJson() 0 9 1
A produceMobi() 0 40 3
A produceCsv() 0 37 2
A producePdf() 0 65 4
A __construct() 0 11 4
A updateAuthor() 0 16 3
1
<?php
2
3
namespace Wallabag\CoreBundle\Helper;
4
5
use Html2Text\Html2Text;
6
use JMS\Serializer\SerializationContext;
7
use JMS\Serializer\SerializerBuilder;
8
use PHPePub\Core\EPub;
9
use PHPePub\Core\Structure\OPF\DublinCore;
10
use Symfony\Component\HttpFoundation\Response;
11
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
12
use Symfony\Component\Translation\TranslatorInterface;
13
use Wallabag\CoreBundle\Entity\Entry;
14
use Wallabag\UserBundle\Entity\User;
15
16
/**
17
 * This class doesn't have unit test BUT it's fully covered by a functional test with ExportControllerTest.
18
 */
19
class EntriesExport
20
{
21
    private $wallabagUrl;
22
    private $logoPath;
23
    private $translator;
24
    private $title = '';
25
    private $entries = [];
26
    private $author = 'wallabag';
27
    private $language = '';
28
    private $user;
29
30
    /**
31
     * @param TranslatorInterface   $translator   Translator service
32
     * @param string                $wallabagUrl  Wallabag instance url
33
     * @param string                $logoPath     Path to the logo FROM THE BUNDLE SCOPE
34
     * @param TokenStorageInterface $tokenStorage Needed to retrieve the current user
35
     */
36
    public function __construct(TranslatorInterface $translator, $wallabagUrl, $logoPath, TokenStorageInterface $tokenStorage)
37
    {
38
        $this->translator = $translator;
39
        $this->wallabagUrl = $wallabagUrl;
40
        $this->logoPath = $logoPath;
41
42
        /* @var User $user */
43
        $this->user = $tokenStorage->getToken() ? $tokenStorage->getToken()->getUser() : null;
44
45
        if (null === $this->user || !\is_object($this->user)) {
46
            return;
47
        }
48
    }
49
50
    /**
51
     * Define entries.
52
     *
53
     * @param array|Entry $entries An array of entries or one entry
54
     *
55
     * @return EntriesExport
56
     */
57
    public function setEntries($entries)
58
    {
59
        if (!\is_array($entries)) {
60
            $this->language = $entries->getLanguage();
61
            $entries = [$entries];
62
        }
63
64
        $this->entries = $entries;
65
66
        return $this;
67
    }
68
69
    /**
70
     * Sets the category of which we want to get articles, or just one entry.
71
     *
72
     * @param string $method Method to get articles
73
     *
74
     * @return EntriesExport
75
     */
76
    public function updateTitle($method)
77
    {
78
        $this->title = $method . ' articles';
79
80
        if ('entry' === $method) {
81
            $this->title = $this->entries[0]->getTitle();
82
        }
83
84
        return $this;
85
    }
86
87
    /**
88
     * Sets the author for one entry or category.
89
     *
90
     * The publishers are used, or the domain name if empty.
91
     *
92
     * @param string $method Method to get articles
93
     *
94
     * @return EntriesExport
95
     */
96
    public function updateAuthor($method)
97
    {
98
        if ('entry' !== $method) {
99
            $this->author = 'Various authors';
100
101
            return $this;
102
        }
103
104
        $this->author = $this->entries[0]->getDomainName();
105
106
        $publishedBy = $this->entries[0]->getPublishedBy();
107
        if (!empty($publishedBy)) {
108
            $this->author = implode(', ', $publishedBy);
109
        }
110
111
        return $this;
112
    }
113
114
    /**
115
     * Sets the output format.
116
     *
117
     * @param string $format
118
     *
119
     * @return Response
120
     */
121
    public function exportAs($format)
122
    {
123
        $functionName = 'produce' . ucfirst($format);
124
        if (method_exists($this, $functionName)) {
125
            return $this->$functionName();
126
        }
127
128
        throw new \InvalidArgumentException(sprintf('The format "%s" is not yet supported.', $format));
129
    }
130
131
    public function exportJsonData()
132
    {
133
        return $this->prepareSerializingContent('json');
134
    }
135
136
    /**
137
     * Use PHPePub to dump a .epub file.
138
     *
139
     * @return Response
140
     */
141
    private function produceEpub()
142
    {
143
        /*
144
         * Start and End of the book
145
         */
146
        $content_start =
147
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
148
            . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
149
            . '<head>'
150
            . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
151
            . "<title>wallabag articles book</title>\n"
152
            . "</head>\n"
153
            . "<body>\n";
154
155
        $bookEnd = "</body>\n</html>\n";
156
157
        $book = new EPub(EPub::BOOK_VERSION_EPUB3);
158
159
        /*
160
         * Book metadata
161
         */
162
163
        $book->setTitle($this->title);
164
        // 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.
165
        $book->setLanguage($this->language);
166
        $book->setDescription('Some articles saved on my wallabag');
167
168
        $book->setAuthor($this->author, $this->author);
169
170
        // I hope this is a non existant address :)
171
        $book->setPublisher('wallabag', 'wallabag');
172
        // Strictly not needed as the book date defaults to time().
173
        $book->setDate(time());
174
        $book->setSourceURL($this->wallabagUrl);
175
176
        $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'PHP');
177
        $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, 'wallabag');
178
179
        $entryIds = [];
180
        $entryCount = \count($this->entries);
181
        $i = 0;
182
183
        /*
184
         * Adding actual entries
185
         */
186
187
        // set tags as subjects
188
        foreach ($this->entries as $entry) {
189
            ++$i;
190
191
            /*
192
             * Front page
193
             * Set if there's only one entry in the given set
194
             */
195
            if (1 === $entryCount && null !== $entry->getPreviewPicture()) {
196
                $book->setCoverImage($entry->getPreviewPicture());
197
            }
198
199
            foreach ($entry->getTags() as $tag) {
200
                $book->setSubject($tag->getLabel());
201
            }
202
            $filename = sha1(sprintf('%s:%s', $entry->getUrl(), $entry->getTitle()));
203
204
            $publishedBy = $entry->getPublishedBy();
205
            $authors = $this->translator->trans('export.unknown');
206
            if (!empty($publishedBy)) {
207
                $authors = implode(',', $publishedBy);
208
            }
209
210
            $publishedAt = $entry->getPublishedAt();
211
            $publishedDate = $this->translator->trans('export.unknown');
212
            if (!empty($publishedAt)) {
213
                $publishedDate = $entry->getPublishedAt()->format('Y-m-d');
214
            }
215
216
            $readingTime = $entry->getReadingTime() / $this->user->getConfig()->getReadingSpeed() * 200;
217
218
            $titlepage = $content_start .
219
                '<h1>' . $entry->getTitle() . '</h1>' .
220
                '<dl>' .
221
                '<dt>' . $this->translator->trans('entry.view.published_by') . '</dt><dd>' . $authors . '</dd>' .
222
                '<dt>' . $this->translator->trans('entry.metadata.published_on') . '</dt><dd>' . $publishedDate . '</dd>' .
223
                '<dt>' . $this->translator->trans('entry.metadata.reading_time') . '</dt><dd>' . $this->translator->trans('entry.metadata.reading_time_minutes_short', ['%readingTime%' => $readingTime]) . '</dd>' .
224
                '<dt>' . $this->translator->trans('entry.metadata.added_on') . '</dt><dd>' . $entry->getCreatedAt()->format('Y-m-d') . '</dd>' .
225
                '<dt>' . $this->translator->trans('entry.metadata.address') . '</dt><dd><a href="' . $entry->getUrl() . '">' . $entry->getUrl() . '</a></dd>' .
226
                '</dl>' .
227
                $bookEnd;
228
            $book->addChapter("Entry {$i} of {$entryCount}", "{$filename}_cover.html", $titlepage, true, EPub::EXTERNAL_REF_ADD);
229
            $chapter = $content_start . $entry->getContent() . $bookEnd;
230
231
            $entryIds[] = $entry->getId();
232
            $book->addChapter($entry->getTitle(), "{$filename}.html", $chapter, true, EPub::EXTERNAL_REF_ADD);
233
        }
234
235
        $book->addChapter('Notices', 'Cover2.html', $content_start . $this->getExportInformation('PHPePub') . $bookEnd);
236
237
        // Could also be the ISBN number, prefered for published books, or a UUID.
238
        $hash = sha1(sprintf('%s:%s', $this->wallabagUrl, implode(',', $entryIds)));
239
        $book->setIdentifier(sprintf('urn:wallabag:%s', $hash), EPub::IDENTIFIER_URI);
240
241
        return Response::create(
242
            $book->getBook(),
243
            200,
244
            [
245
                'Content-Description' => 'File Transfer',
246
                'Content-type' => 'application/epub+zip',
247
                'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.epub"',
248
                'Content-Transfer-Encoding' => 'binary',
249
            ]
250
        );
251
    }
252
253
    /**
254
     * Use PHPMobi to dump a .mobi file.
255
     *
256
     * @return Response
257
     */
258
    private function produceMobi()
259
    {
260
        $mobi = new \MOBI();
261
        $content = new \MOBIFile();
262
263
        /*
264
         * Book metadata
265
         */
266
        $content->set('title', $this->title);
267
        $content->set('author', $this->author);
268
        $content->set('subject', $this->title);
269
270
        /*
271
         * Front page
272
         */
273
        $content->appendParagraph($this->getExportInformation('PHPMobi'));
274
        if (file_exists($this->logoPath)) {
275
            $content->appendImage(imagecreatefrompng($this->logoPath));
276
        }
277
        $content->appendPageBreak();
278
279
        /*
280
         * Adding actual entries
281
         */
282
        foreach ($this->entries as $entry) {
283
            $content->appendChapterTitle($entry->getTitle());
284
            $content->appendParagraph($entry->getContent());
285
            $content->appendPageBreak();
286
        }
287
        $mobi->setContentProvider($content);
288
289
        return Response::create(
290
            $mobi->toString(),
291
            200,
292
            [
293
                'Accept-Ranges' => 'bytes',
294
                'Content-Description' => 'File Transfer',
295
                'Content-type' => 'application/x-mobipocket-ebook',
296
                'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.mobi"',
297
                'Content-Transfer-Encoding' => 'binary',
298
            ]
299
        );
300
    }
301
302
    /**
303
     * Use TCPDF to dump a .pdf file.
304
     *
305
     * @return Response
306
     */
307
    private function producePdf()
308
    {
309
        $pdf = new \TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
310
311
        /*
312
         * Book metadata
313
         */
314
        $pdf->SetCreator(PDF_CREATOR);
315
        $pdf->SetAuthor($this->author);
316
        $pdf->SetTitle($this->title);
317
        $pdf->SetSubject('Articles via wallabag');
318
        $pdf->SetKeywords('wallabag');
319
320
        /*
321
         * Adding actual entries
322
         */
323
        foreach ($this->entries as $entry) {
324
            foreach ($entry->getTags() as $tag) {
325
                $pdf->SetKeywords($tag->getLabel());
326
            }
327
328
            $publishedBy = $entry->getPublishedBy();
329
            $authors = $this->translator->trans('export.unknown');
330
            if (!empty($publishedBy)) {
331
                $authors = implode(',', $publishedBy);
332
            }
333
334
            $readingTime = $entry->getReadingTime() / $this->user->getConfig()->getReadingSpeed() * 200;
335
336
            $pdf->addPage();
337
            $html = '<h1>' . $entry->getTitle() . '</h1>' .
338
                '<dl>' .
339
                '<dt>' . $this->translator->trans('entry.view.published_by') . '</dt><dd>' . $authors . '</dd>' .
340
                '<dt>' . $this->translator->trans('entry.metadata.reading_time') . '</dt><dd>' . $this->translator->trans('entry.metadata.reading_time_minutes_short', ['%readingTime%' => $readingTime]) . '</dd>' .
341
                '<dt>' . $this->translator->trans('entry.metadata.added_on') . '</dt><dd>' . $entry->getCreatedAt()->format('Y-m-d') . '</dd>' .
342
                '<dt>' . $this->translator->trans('entry.metadata.address') . '</dt><dd><a href="' . $entry->getUrl() . '">' . $entry->getUrl() . '</a></dd>' .
343
                '</dl>';
344
            $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true);
345
346
            $pdf->AddPage();
347
            $html = '<h1>' . $entry->getTitle() . '</h1>';
348
            $html .= $entry->getContent();
349
350
            $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true);
351
        }
352
353
        /*
354
         * Last page
355
         */
356
        $pdf->AddPage();
357
        $html = $this->getExportInformation('tcpdf');
358
359
        $pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true);
360
361
        // set image scale factor
362
        $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
363
364
        return Response::create(
365
            $pdf->Output('', 'S'),
366
            200,
367
            [
368
                'Content-Description' => 'File Transfer',
369
                'Content-type' => 'application/pdf',
370
                'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.pdf"',
371
                'Content-Transfer-Encoding' => 'binary',
372
            ]
373
        );
374
    }
375
376
    /**
377
     * Inspired from CsvFileDumper.
378
     *
379
     * @return Response
380
     */
381
    private function produceCsv()
382
    {
383
        $delimiter = ';';
384
        $enclosure = '"';
385
        $handle = fopen('php://memory', 'b+r');
386
387
        fputcsv($handle, ['Title', 'URL', 'Content', 'Tags', 'MIME Type', 'Language', 'Creation date'], $delimiter, $enclosure);
388
389
        foreach ($this->entries as $entry) {
390
            fputcsv(
391
                $handle,
392
                [
393
                    $entry->getTitle(),
394
                    $entry->getURL(),
395
                    // remove new line to avoid crazy results
396
                    str_replace(["\r\n", "\r", "\n"], '', $entry->getContent()),
397
                    implode(', ', $entry->getTags()->toArray()),
398
                    $entry->getMimetype(),
399
                    $entry->getLanguage(),
400
                    $entry->getCreatedAt()->format('d/m/Y h:i:s'),
401
                ],
402
                $delimiter,
403
                $enclosure
404
            );
405
        }
406
407
        rewind($handle);
408
        $output = stream_get_contents($handle);
409
        fclose($handle);
410
411
        return Response::create(
412
            $output,
413
            200,
414
            [
415
                'Content-type' => 'application/csv',
416
                'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.csv"',
417
                'Content-Transfer-Encoding' => 'UTF-8',
418
            ]
419
        );
420
    }
421
422
    /**
423
     * Dump a JSON file.
424
     *
425
     * @return Response
426
     */
427
    private function produceJson()
428
    {
429
        return Response::create(
430
            $this->prepareSerializingContent('json'),
431
            200,
432
            [
433
                'Content-type' => 'application/json',
434
                'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.json"',
435
                'Content-Transfer-Encoding' => 'UTF-8',
436
            ]
437
        );
438
    }
439
440
    /**
441
     * Dump a XML file.
442
     *
443
     * @return Response
444
     */
445
    private function produceXml()
446
    {
447
        return Response::create(
448
            $this->prepareSerializingContent('xml'),
449
            200,
450
            [
451
                'Content-type' => 'application/xml',
452
                'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.xml"',
453
                'Content-Transfer-Encoding' => 'UTF-8',
454
            ]
455
        );
456
    }
457
458
    /**
459
     * Dump a TXT file.
460
     *
461
     * @return Response
462
     */
463
    private function produceTxt()
464
    {
465
        $content = '';
466
        $bar = str_repeat('=', 100);
467
        foreach ($this->entries as $entry) {
468
            $content .= "\n\n" . $bar . "\n\n" . $entry->getTitle() . "\n\n" . $bar . "\n\n";
469
            $html = new Html2Text($entry->getContent(), ['do_links' => 'none', 'width' => 100]);
470
            $content .= $html->getText();
471
        }
472
473
        return Response::create(
474
            $content,
475
            200,
476
            [
477
                'Content-type' => 'text/plain',
478
                'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.txt"',
479
                'Content-Transfer-Encoding' => 'UTF-8',
480
            ]
481
        );
482
    }
483
484
    /**
485
     * Return a Serializer object for producing processes that need it (JSON & XML).
486
     *
487
     * @param string $format
488
     *
489
     * @return string
490
     */
491
    private function prepareSerializingContent($format)
492
    {
493
        $serializer = SerializerBuilder::create()->build();
494
495
        return $serializer->serialize(
496
            $this->entries,
497
            $format,
498
            SerializationContext::create()->setGroups(['entries_for_user'])
499
        );
500
    }
501
502
    /**
503
     * Return a kind of footer / information for the epub.
504
     *
505
     * @param string $type Generator of the export, can be: tdpdf, PHPePub, PHPMobi
506
     *
507
     * @return string
508
     */
509
    private function getExportInformation($type)
510
    {
511
        $info = $this->translator->trans('export.footer_template', [
512
            '%method%' => $type,
513
        ]);
514
515
        if ('tcpdf' === $type) {
516
            return str_replace('%IMAGE%', '<img src="' . $this->logoPath . '" />', $info);
517
        }
518
519
        return str_replace('%IMAGE%', '', $info);
520
    }
521
522
    /**
523
     * Return a sanitized version of the title by applying translit iconv
524
     * and removing non alphanumeric characters, - and space.
525
     *
526
     * @return string Sanitized filename
527
     */
528
    private function getSanitizedFilename()
529
    {
530
        return preg_replace('/[^A-Za-z0-9\- \']/', '', iconv('utf-8', 'us-ascii//TRANSLIT', $this->title));
531
    }
532
}
533