AbstractGenerator::__destruct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Knp\Snappy;
4
5
use Knp\Snappy\Exception\FileAlreadyExistsException;
6
use Psr\Log\LoggerAwareInterface;
7
use Psr\Log\LoggerInterface;
8
use Psr\Log\NullLogger;
9
use Symfony\Component\Process\Process;
10
use Exception;
11
use LogicException;
12
use RuntimeException;
13
use InvalidArgumentException;
14
15
/**
16
 * Base generator class for medias.
17
 *
18
 * @author  Matthieu Bontemps <[email protected]>
19
 * @author  Antoine Hérault <[email protected]>
20
 */
21
abstract class AbstractGenerator implements GeneratorInterface, LoggerAwareInterface
22
{
23
    /**
24
     * @var array
25
     */
26
    public $temporaryFiles = [];
27
28
    /**
29
     * @var string
30
     */
31
    protected $temporaryFolder;
32
33
    /**
34
     * @var null|string
35
     */
36
    private $binary;
37
38
    /**
39
     * @var array
40
     */
41
    private $options = [];
42
43
    /**
44
     * @var null|array
45
     */
46
    private $env;
47
48
    /**
49
     * @var null|int
50
     */
51
    private $timeout;
52
53
    /**
54
     * @var string
55
     */
56
    private $defaultExtension;
57
58
    /**
59
     * @var LoggerInterface
60
     */
61
    private $logger;
62
63
    /**
64
     * @param null|string $binary
65
     * @param array       $options
66
     * @param null|array  $env
67
     */
68
    public function __construct(string $binary = null, array $options = [], array $env = null)
69
    {
70
        $this->configure();
71
72
        $this->logger = new NullLogger();
73
        $this->setBinary($binary);
74
        $this->setOptions($options);
75
        $this->env = empty($env) ? null : $env;
76
77
        if (\is_callable([$this, 'removeTemporaryFiles'])) {
78
            \register_shutdown_function([$this, 'removeTemporaryFiles']);
79
        }
80
    }
81
82
    public function __destruct()
83
    {
84
        $this->removeTemporaryFiles();
85
    }
86
87
    /**
88
     * Set the logger to use to log debugging data.
89
     *
90
     * @param LoggerInterface $logger
91
     */
92
    public function setLogger(LoggerInterface $logger): self
93
    {
94
        $this->logger = $logger;
95
96
        return $this;
97
    }
98
99
    /**
100
     * Sets the default extension.
101
     * Useful when letting Snappy deal with file creation.
102
     *
103
     * @param string $defaultExtension
104
     */
105
    public function setDefaultExtension(string $defaultExtension): self
106
    {
107
        $this->defaultExtension = $defaultExtension;
108
109
        return $this;
110
    }
111
112
    /**
113
     * Gets the default extension.
114
     *
115
     * @return string
116
     */
117
    public function getDefaultExtension(): string
118
    {
119
        return $this->defaultExtension;
120
    }
121
122
    /**
123
     * Sets an option. Be aware that option values are NOT validated and that
124
     * it is your responsibility to validate user inputs.
125
     *
126
     * @param string $name  The option to set
127
     * @param mixed  $value The value (NULL to unset)
128
     *
129
     * @throws InvalidArgumentException
130
     */
131
    public function setOption(string $name, $value): self
132
    {
133
        if (!\array_key_exists($name, $this->options)) {
134
            throw new InvalidArgumentException(\sprintf('The option \'%s\' does not exist.', $name));
135
        }
136
137
        $this->options[$name] = $value;
138
139
        $this->logger->debug(\sprintf('Set option "%s".', $name), ['value' => $value]);
140
141
        return $this;
142
    }
143
144
    /**
145
     * Sets the timeout.
146
     *
147
     * @param null|int $timeout The timeout to set
148
     */
149
    public function setTimeout(?int $timeout): self
150
    {
151
        $this->timeout = $timeout;
152
153
        return $this;
154
    }
155
156
    /**
157
     * Sets an array of options.
158
     *
159
     * @param array $options An associative array of options as name/value
160
     */
161
    public function setOptions(array $options): self
162
    {
163
        foreach ($options as $name => $value) {
164
            $this->setOption($name, $value);
165
        }
166
167
        return $this;
168
    }
169
170
    /**
171
     * Returns all the options.
172
     *
173
     * @return array
174
     */
175
    public function getOptions(): array
176
    {
177
        return $this->options;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183
    public function generate($input, $output, array $options = [], $overwrite = false)
184
    {
185
        $this->prepareOutput($output, $overwrite);
186
187
        $command = $this->getCommand($input, $output, $options);
188
189
        $inputFiles = \is_array($input) ? \implode('", "', $input) : $input;
190
191
        $this->logger->info(\sprintf('Generate from file(s) "%s" to file "%s".', $inputFiles, $output), [
192
            'command' => $command,
193
            'env' => $this->env,
194
            'timeout' => $this->timeout,
195
        ]);
196
197
        try {
198
            list($status, $stdout, $stderr) = $this->executeCommand($command);
199
            $this->checkProcessStatus($status, $stdout, $stderr, $command);
200
            $this->checkOutput($output, $command);
201
        } catch (Exception $e) {
202
            $this->logger->error(\sprintf('An error happened while generating "%s".', $output), [
203
                'command' => $command,
204
                'status' => $status ?? null,
205
                'stdout' => $stdout ?? null,
206
                'stderr' => $stderr ?? null,
207
            ]);
208
209
            throw $e;
210
        }
211
212
        $this->logger->info(\sprintf('File "%s" has been successfully generated.', $output), [
213
            'command' => $command,
214
            'stdout' => $stdout,
215
            'stderr' => $stderr,
216
        ]);
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222
    public function generateFromHtml($html, $output, array $options = [], $overwrite = false)
223
    {
224
        $fileNames = [];
225
        if (\is_array($html)) {
226
            foreach ($html as $htmlInput) {
227
                $fileNames[] = $this->createTemporaryFile($htmlInput, 'html');
228
            }
229
        } else {
230
            $fileNames[] = $this->createTemporaryFile($html, 'html');
231
        }
232
233
        $this->generate($fileNames, $output, $options, $overwrite);
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function getOutput($input, array $options = [])
240
    {
241
        $filename = $this->createTemporaryFile(null, $this->getDefaultExtension());
242
243
        $this->generate($input, $filename, $options);
244
245
        return $this->getFileContents($filename);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function getOutputFromHtml($html, array $options = [])
252
    {
253
        $fileNames = [];
254
        if (\is_array($html)) {
255
            foreach ($html as $htmlInput) {
256
                $fileNames[] = $this->createTemporaryFile($htmlInput, 'html');
257
            }
258
        } else {
259
            $fileNames[] = $this->createTemporaryFile($html, 'html');
260
        }
261
262
        return $this->getOutput($fileNames, $options);
263
    }
264
265
    /**
266
     * Defines the binary.
267
     *
268
     * @param null|string $binary The path/name of the binary
269
     */
270
    public function setBinary(?string $binary): self
271
    {
272
        $this->binary = $binary;
273
274
        return $this;
275
    }
276
277
    /**
278
     * Returns the binary.
279
     *
280
     * @return null|string
281
     */
282
    public function getBinary(): ?string
283
    {
284
        return $this->binary;
285
    }
286
287
    /**
288
     * Returns the command for the given input and output files.
289
     *
290
     * @param array|string $input   The input file
291
     * @param string       $output  The ouput file
292
     * @param array        $options An optional array of options that will be used
293
     *                              only for this command
294
     *
295
     * @return string
296
     */
297
    public function getCommand($input, string $output, array $options = []): string
298
    {
299
        if (null === $this->binary) {
300
            throw new LogicException('You must define a binary prior to conversion.');
301
        }
302
303
        $options = $this->mergeOptions($options);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $options. This often makes code more readable.
Loading history...
304
305
        return $this->buildCommand($this->binary, $input, $output, $options);
306
    }
307
308
    /**
309
     * Removes all temporary files.
310
     */
311
    public function removeTemporaryFiles(): void
312
    {
313
        foreach ($this->temporaryFiles as $file) {
314
            $this->unlink($file);
315
        }
316
    }
317
318
    /**
319
     * Get TemporaryFolder.
320
     *
321
     * @return string
322
     */
323
    public function getTemporaryFolder(): string
324
    {
325
        if ($this->temporaryFolder === null) {
326
            return \sys_get_temp_dir();
327
        }
328
329
        return $this->temporaryFolder;
330
    }
331
332
    /**
333
     * Set temporaryFolder.
334
     *
335
     * @param string $temporaryFolder
336
     *
337
     * @return $this
338
     */
339
    public function setTemporaryFolder(string $temporaryFolder): self
340
    {
341
        $this->temporaryFolder = $temporaryFolder;
342
343
        return $this;
344
    }
345
346
    /**
347
     * Reset all options to their initial values.
348
     *
349
     * @return void
350
     */
351
    public function resetOptions(): void
352
    {
353
        $this->options = [];
354
        $this->configure();
355
    }
356
357
    /**
358
     * This method must configure the media options.
359
     *
360
     * @return void
361
     *
362
     * @see AbstractGenerator::addOption()
363
     */
364
    abstract protected function configure(): void;
365
366
    /**
367
     * Adds an option.
368
     *
369
     * @param string $name    The name
370
     * @param mixed  $default An optional default value
371
     *
372
     * @throws InvalidArgumentException
373
     */
374
    protected function addOption(string $name, $default = null): self
375
    {
376
        if (\array_key_exists($name, $this->options)) {
377
            throw new InvalidArgumentException(\sprintf('The option \'%s\' already exists.', $name));
378
        }
379
380
        $this->options[$name] = $default;
381
382
        return $this;
383
    }
384
385
    /**
386
     * Adds an array of options.
387
     *
388
     * @param array $options
389
     */
390
    protected function addOptions(array $options): self
391
    {
392
        foreach ($options as $name => $default) {
393
            $this->addOption($name, $default);
394
        }
395
396
        return $this;
397
    }
398
399
    /**
400
     * Merges the given array of options to the instance options and returns
401
     * the result options array. It does NOT change the instance options.
402
     *
403
     * @param array $options
404
     *
405
     * @throws InvalidArgumentException
406
     *
407
     * @return array
408
     */
409
    protected function mergeOptions(array $options): array
410
    {
411
        $mergedOptions = $this->options;
412
413
        foreach ($options as $name => $value) {
414
            if (!\array_key_exists($name, $mergedOptions)) {
415
                throw new InvalidArgumentException(\sprintf('The option \'%s\' does not exist.', $name));
416
            }
417
418
            $mergedOptions[$name] = $value;
419
        }
420
421
        return $mergedOptions;
422
    }
423
424
    /**
425
     * Checks the specified output.
426
     *
427
     * @param string $output  The output filename
428
     * @param string $command The generation command
429
     *
430
     * @throws RuntimeException if the output file generation failed
431
     */
432
    protected function checkOutput(string $output, string $command): void
433
    {
434
        // the output file must exist
435
        if (!$this->fileExists($output)) {
436
            throw new RuntimeException(\sprintf('The file \'%s\' was not created (command: %s).', $output, $command));
437
        }
438
439
        // the output file must not be empty
440
        if (0 === $this->filesize($output)) {
441
            throw new RuntimeException(\sprintf('The file \'%s\' was created but is empty (command: %s).', $output, $command));
442
        }
443
    }
444
445
    /**
446
     * Checks the process return status.
447
     *
448
     * @param int    $status  The exit status code
449
     * @param string $stdout  The stdout content
450
     * @param string $stderr  The stderr content
451
     * @param string $command The run command
452
     *
453
     * @throws RuntimeException if the output file generation failed
454
     */
455
    protected function checkProcessStatus(int $status, string $stdout, string $stderr, string $command): void
456
    {
457
        if (0 !== $status && '' !== $stderr) {
458
            throw new RuntimeException(\sprintf('The exit status code \'%s\' says something went wrong:' . "\n" . 'stderr: "%s"' . "\n" . 'stdout: "%s"' . "\n" . 'command: %s.', $status, $stderr, $stdout, $command), $status);
459
        }
460
    }
461
462
    /**
463
     * Creates a temporary file.
464
     * The file is not created if the $content argument is null.
465
     *
466
     * @param null|string $content   Optional content for the temporary file
467
     * @param null|string $extension An optional extension for the filename
468
     *
469
     * @return string The filename
470
     */
471
    protected function createTemporaryFile(?string $content = null, ?string $extension = null): string
472
    {
473
        $dir = \rtrim($this->getTemporaryFolder(), \DIRECTORY_SEPARATOR);
474
475
        if (!\is_dir($dir)) {
476
            if (false === @\mkdir($dir, 0777, true) && !\is_dir($dir)) {
477
                throw new RuntimeException(\sprintf("Unable to create directory: %s\n", $dir));
478
            }
479
        } elseif (!\is_writable($dir)) {
480
            throw new RuntimeException(\sprintf("Unable to write in directory: %s\n", $dir));
481
        }
482
483
        $filename = $dir . \DIRECTORY_SEPARATOR . \uniqid('knp_snappy', true);
484
485
        if (null !== $extension) {
486
            $filename .= '.' . $extension;
487
        }
488
489
        if (null !== $content) {
490
            \file_put_contents($filename, $content);
491
        }
492
493
        $this->temporaryFiles[] = $filename;
494
495
        return $filename;
496
    }
497
498
    /**
499
     * Builds the command string.
500
     *
501
     * @param string       $binary  The binary path/name
502
     * @param array|string $input   Url(s) or file location(s) of the page(s) to process
503
     * @param string       $output  File location to the image-to-be
504
     * @param array        $options An array of options
505
     *
506
     * @return string
507
     */
508
    protected function buildCommand(string $binary, $input, string $output, array $options = []): string
509
    {
510
        $command = $binary;
511
        $escapedBinary = \escapeshellarg($binary);
512
        if (\is_executable($escapedBinary)) {
513
            $command = $escapedBinary;
514
        }
515
516
        foreach ($options as $key => $option) {
517
            if (null !== $option && false !== $option) {
518
                if (true === $option) {
519
                    // Dont't put '--' if option is 'toc'.
520
                    if ($key === 'toc') {
521
                        $command .= ' ' . $key;
522
                    } else {
523
                        $command .= ' --' . $key;
524
                    }
525
                } elseif (\is_array($option)) {
526
                    if ($this->isAssociativeArray($option)) {
527
                        foreach ($option as $k => $v) {
528
                            $command .= ' --' . $key . ' ' . \escapeshellarg($k) . ' ' . \escapeshellarg($v);
529
                        }
530
                    } else {
531
                        foreach ($option as $v) {
532
                            $command .= ' --' . $key . ' ' . \escapeshellarg($v);
533
                        }
534
                    }
535
                } else {
536
                    // Dont't add '--' if option is "cover"  or "toc".
537
                    if (\in_array($key, ['toc', 'cover'])) {
538
                        $command .= ' ' . $key . ' ' . \escapeshellarg($option);
539
                    } elseif (\in_array($key, ['image-dpi', 'image-quality'])) {
540
                        $command .= ' --' . $key . ' ' . (int) $option;
541
                    } else {
542
                        $command .= ' --' . $key . ' ' . \escapeshellarg($option);
543
                    }
544
                }
545
            }
546
        }
547
548
        if (\is_array($input)) {
549
            foreach ($input as $i) {
550
                $command .= ' ' . \escapeshellarg($i) . ' ';
551
            }
552
            $command .= \escapeshellarg($output);
553
        } else {
554
            $command .= ' ' . \escapeshellarg($input) . ' ' . \escapeshellarg($output);
555
        }
556
557
        return $command;
558
    }
559
560
    /**
561
     * Return true if the array is an associative array
562
     * and not an indexed array.
563
     *
564
     * @param array $array
565
     *
566
     * @return bool
567
     */
568
    protected function isAssociativeArray(array $array): bool
569
    {
570
        return (bool) \count(\array_filter(\array_keys($array), 'is_string'));
571
    }
572
573
    /**
574
     * Executes the given command via shell and returns the complete output as
575
     * a string.
576
     *
577
     * @param string $command
578
     *
579
     * @return array [status, stdout, stderr]
580
     */
581
    protected function executeCommand(string $command): array
582
    {
583
        if (\method_exists(Process::class, 'fromShellCommandline')) {
584
            $process = Process::fromShellCommandline($command, null, $this->env);
585
        } else {
586
            $process = new Process($command, null, $this->env);
0 ignored issues
show
Documentation introduced by
$command is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
587
        }
588
589
        if (null !== $this->timeout) {
590
            $process->setTimeout($this->timeout);
591
        }
592
593
        $process->run();
594
595
        return [
596
            $process->getExitCode(),
597
            $process->getOutput(),
598
            $process->getErrorOutput(),
599
        ];
600
    }
601
602
    /**
603
     * Prepares the specified output.
604
     *
605
     * @param string $filename  The output filename
606
     * @param bool   $overwrite Whether to overwrite the file if it already
607
     *                          exist
608
     *
609
     * @throws FileAlreadyExistsException
610
     * @throws RuntimeException
611
     * @throws InvalidArgumentException
612
     */
613
    protected function prepareOutput(string $filename, bool $overwrite): void
614
    {
615
        $directory = \dirname($filename);
616
617
        if ($this->fileExists($filename)) {
618
            if (!$this->isFile($filename)) {
619
                throw new InvalidArgumentException(\sprintf('The output file \'%s\' already exists and it is a %s.', $filename, $this->isDir($filename) ? 'directory' : 'link'));
620
            }
621
            if (false === $overwrite) {
622
                throw new FileAlreadyExistsException(\sprintf('The output file \'%s\' already exists.', $filename));
623
            }
624
            if (!$this->unlink($filename)) {
625
                throw new RuntimeException(\sprintf('Could not delete already existing output file \'%s\'.', $filename));
626
            }
627
        } elseif (!$this->isDir($directory) && !$this->mkdir($directory)) {
628
            throw new RuntimeException(\sprintf('The output file\'s directory \'%s\' could not be created.', $directory));
629
        }
630
    }
631
632
    /**
633
     * Wrapper for the "file_get_contents" function.
634
     *
635
     * @param string $filename
636
     *
637
     * @return string
638
     */
639
    protected function getFileContents(string $filename): string
640
    {
641
        $fileContent = \file_get_contents($filename);
642
643
        if (false === $fileContent) {
644
            throw new RuntimeException(\sprintf('Could not read file \'%s\' content.', $filename));
645
        }
646
647
        return $fileContent;
648
    }
649
650
    /**
651
     * Wrapper for the "file_exists" function.
652
     *
653
     * @param string $filename
654
     *
655
     * @return bool
656
     */
657
    protected function fileExists(string $filename): bool
658
    {
659
        return \file_exists($filename);
660
    }
661
662
    /**
663
     * Wrapper for the "is_file" method.
664
     *
665
     * @param string $filename
666
     *
667
     * @return bool
668
     */
669
    protected function isFile(string $filename): bool
670
    {
671
        return \strlen($filename) <= \PHP_MAXPATHLEN && \is_file($filename);
672
    }
673
674
    /**
675
     * Wrapper for the "filesize" function.
676
     *
677
     * @param string $filename
678
     *
679
     * @return int
680
     */
681
    protected function filesize(string $filename): int
682
    {
683
        $filesize = \filesize($filename);
684
685
        if (false === $filesize) {
686
            throw new RuntimeException(\sprintf('Could not read file \'%s\' size.', $filename));
687
        }
688
689
        return $filesize;
690
    }
691
692
    /**
693
     * Wrapper for the "unlink" function.
694
     *
695
     * @param string $filename
696
     *
697
     * @return bool
698
     */
699
    protected function unlink(string $filename): bool
700
    {
701
        return $this->fileExists($filename) ? \unlink($filename) : false;
702
    }
703
704
    /**
705
     * Wrapper for the "is_dir" function.
706
     *
707
     * @param string $filename
708
     *
709
     * @return bool
710
     */
711
    protected function isDir(string $filename): bool
712
    {
713
        return \is_dir($filename);
714
    }
715
716
    /**
717
     * Wrapper for the mkdir function.
718
     *
719
     * @param string $pathname
720
     *
721
     * @return bool
722
     */
723
    protected function mkdir(string $pathname): bool
724
    {
725
        return \mkdir($pathname, 0777, true);
726
    }
727
}
728