Passed
Push — master ( 036635...362583 )
by Jeremy
08:25
created

EntriesExport::produceEpub()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 97
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 56
nc 10
nop 0
dl 0
loc 97
rs 8.6488
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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