Completed
Push — master ( 3bd249...1db42f )
by Albin
12s
created

AbstractGenerator::setTemporaryFolder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Knp\Snappy;
4
5
use Knp\Snappy\Exception as Exceptions;
6
use Psr\Log\LoggerInterface;
7
use Psr\Log\NullLogger;
8
use Symfony\Component\Process\Process;
9
10
/**
11
 * Base generator class for medias
12
 *
13
 * @package Snappy
14
 *
15
 * @author  Matthieu Bontemps <[email protected]>
16
 * @author  Antoine Hérault <[email protected]>
17
 */
18
abstract class AbstractGenerator implements GeneratorInterface
19
{
20
    private $binary;
21
    private $options = [];
22
    private $env;
23
    private $timeout = false;
24
    private $defaultExtension;
25
26
    /**
27
     * @var string
28
     */
29
    protected $temporaryFolder;
30
31
    /**
32
     * @var array
33
     */
34
    public $temporaryFiles = [];
35
36
    /**
37
     * @var LoggerInterface
38
     */
39
    private $logger;
40
41
    /**
42
     * Constructor
43
     *
44
     * @param string $binary
45
     * @param array  $options
46
     * @param array  $env
47
     */
48
    public function __construct($binary, array $options = [], array $env = null)
49
    {
50
        $this->configure();
51
52
        $this->setBinary($binary);
53
        $this->setOptions($options);
54
        $this->env = empty($env) ? null : $env;
55
        $this->logger = new NullLogger();
56
57
        register_shutdown_function([$this, 'removeTemporaryFiles']);
58
    }
59
60
    public function __destruct()
61
    {
62
        $this->removeTemporaryFiles();
63
    }
64
65
    /**
66
     * Set the logger to use to log debugging data.
67
     *
68
     * @param LoggerInterface $logger
69
     */
70
    public function setLogger(LoggerInterface $logger)
71
    {
72
        $this->logger = $logger;
73
    }
74
75
    /**
76
     * This method must configure the media options
77
     *
78
     * @see AbstractGenerator::addOption()
79
     */
80
    abstract protected function configure();
81
82
    /**
83
     * Sets the default extension.
84
     * Useful when letting Snappy deal with file creation
85
     *
86
     * @param string $defaultExtension
87
     */
88
    public function setDefaultExtension($defaultExtension)
89
    {
90
        $this->defaultExtension = $defaultExtension;
91
    }
92
93
    /**
94
     * Gets the default extension
95
     *
96
     * @return $string
0 ignored issues
show
Documentation introduced by
The doc-type $string could not be parsed: Unknown type name "$string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
97
     */
98
    public function getDefaultExtension()
99
    {
100
        return $this->defaultExtension;
101
    }
102
103
    /**
104
     * Sets an option. Be aware that option values are NOT validated and that
105
     * it is your responsibility to validate user inputs
106
     *
107
     * @param string $name  The option to set
108
     * @param mixed  $value The value (NULL to unset)
109
     *
110
     * @throws \InvalidArgumentException
111
     */
112
    public function setOption($name, $value)
113
    {
114
        if (!array_key_exists($name, $this->options)) {
115
            throw new \InvalidArgumentException(sprintf('The option \'%s\' does not exist.', $name));
116
        }
117
118
        $this->options[$name] = $value;
119
120
        $this->logger->debug(sprintf('Set option "%s" to "%s".', $name, var_export($value, true)));
121
    }
122
123
    /**
124
     * Sets the timeout.
125
     *
126
     * @param integer $timeout The timeout to set
127
     */
128
    public function setTimeout($timeout)
129
    {
130
        $this->timeout = $timeout;
0 ignored issues
show
Documentation Bug introduced by
The property $timeout was declared of type boolean, but $timeout is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
131
    }
132
133
    /**
134
     * Sets an array of options
135
     *
136
     * @param array $options An associative array of options as name/value
137
     */
138
    public function setOptions(array $options)
139
    {
140
        foreach ($options as $name => $value) {
141
            $this->setOption($name, $value);
142
        }
143
    }
144
145
    /**
146
     * Returns all the options
147
     *
148
     * @return array
149
     */
150
    public function getOptions()
151
    {
152
        return $this->options;
153
    }
154
155
    /**
156
     * {@inheritDoc}
157
     */
158
    public function generate($input, $output, array $options = [], $overwrite = false)
159
    {
160
        if (null === $this->binary) {
161
            throw new \LogicException(
162
                'You must define a binary prior to conversion.'
163
            );
164
        }
165
166
        $this->prepareOutput($output, $overwrite);
167
168
        $command = $this->getCommand($input, $output, $options);
169
170
        $inputFiles = is_array($input) ? implode('", "', $input) : $input;
171
172
        $this->logger->info(sprintf('Generate from file(s) "%s" to file "%s".', $inputFiles, $output), [
173
            'command' => $command,
174
            'env'     => $this->env,
175
            'timeout' => $this->timeout,
176
        ]);
177
178
        try {
179
            list($status, $stdout, $stderr) = $this->executeCommand($command);
180
            $this->checkProcessStatus($status, $stdout, $stderr, $command);
181
            $this->checkOutput($output, $command);
182
        } catch (\Exception $e) { // @TODO: should be replaced by \Throwable when support for php5.6 is dropped
183
            $this->logger->error(sprintf('An error happened while generating "%s".', $output), [
184
                'command' => $command,
185
                'status'  => isset($status) ? $status : null,
186
                'stdout'  => isset($stdout) ? $stdout : null,
187
                'stderr'  => isset($stderr) ? $stderr : null,
188
            ]);
189
190
            throw $e;
191
        }
192
193
        $this->logger->info(sprintf('File "%s" has been successfully generated.', $output), [
194
            'command' => $command,
195
            'stdout'  => $stdout,
196
            'stderr'  => $stderr,
197
        ]);
198
    }
199
200
    /**
201
     * {@inheritDoc}
202
     */
203 View Code Duplication
    public function generateFromHtml($html, $output, array $options = [], $overwrite = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
    {
205
        $fileNames = [];
206
        if (is_array($html)) {
207
            foreach ($html as $htmlInput) {
208
                $fileNames[] = $this->createTemporaryFile($htmlInput, 'html');
209
            }
210
        } else {
211
            $fileNames[] = $this->createTemporaryFile($html, 'html');
212
        }
213
214
        $this->generate($fileNames, $output, $options, $overwrite);
215
    }
216
217
    /**
218
     * {@inheritDoc}
219
     */
220
    public function getOutput($input, array $options = [])
221
    {
222
        $filename = $this->createTemporaryFile(null, $this->getDefaultExtension());
223
224
        $this->generate($input, $filename, $options);
225
226
        $result = $this->getFileContents($filename);
227
228
        return $result;
229
    }
230
231
    /**
232
     * {@inheritDoc}
233
     */
234 View Code Duplication
    public function getOutputFromHtml($html, array $options = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
235
    {
236
        $fileNames = [];
237
        if (is_array($html)) {
238
            foreach ($html as $htmlInput) {
239
                $fileNames[] = $this->createTemporaryFile($htmlInput, 'html');
240
            }
241
        } else {
242
            $fileNames[] = $this->createTemporaryFile($html, 'html');
243
        }
244
245
        $result = $this->getOutput($fileNames, $options);
246
247
        return $result;
248
    }
249
250
    /**
251
     * Defines the binary
252
     *
253
     * @param string $binary The path/name of the binary
254
     */
255
    public function setBinary($binary)
256
    {
257
        $this->binary = $binary;
258
    }
259
260
    /**
261
     * Returns the binary
262
     *
263
     * @return string
264
     */
265
    public function getBinary()
266
    {
267
        return $this->binary;
268
    }
269
270
    /**
271
     * Returns the command for the given input and output files
272
     *
273
     * @param array|string  $input      The input file
274
     * @param string        $output     The ouput file
275
     * @param array         $options    An optional array of options that will be used
276
     *                                  only for this command
277
     *
278
     * @return string
279
     */
280
    public function getCommand($input, $output, array $options = [])
281
    {
282
        $options = $this->mergeOptions($options);
283
284
        return $this->buildCommand($this->binary, $input, $output, $options);
285
    }
286
287
    /**
288
     * Adds an option
289
     *
290
     * @param string $name    The name
291
     * @param mixed  $default An optional default value
292
     *
293
     * @throws \InvalidArgumentException
294
     */
295
    protected function addOption($name, $default = null)
296
    {
297
        if (array_key_exists($name, $this->options)) {
298
            throw new \InvalidArgumentException(sprintf('The option \'%s\' already exists.', $name));
299
        }
300
301
        $this->options[$name] = $default;
302
    }
303
304
    /**
305
     * Adds an array of options
306
     *
307
     * @param array $options
308
     */
309
    protected function addOptions(array $options)
310
    {
311
        foreach ($options as $name => $default) {
312
            $this->addOption($name, $default);
313
        }
314
    }
315
316
    /**
317
     * Merges the given array of options to the instance options and returns
318
     * the result options array. It does NOT change the instance options.
319
     *
320
     * @param  array                     $options
321
     * @throws \InvalidArgumentException
322
     *
323
     * @return array
324
     */
325
    protected function mergeOptions(array $options)
326
    {
327
        $mergedOptions = $this->options;
328
329
        foreach ($options as $name => $value) {
330
            if (!array_key_exists($name, $mergedOptions)) {
331
                throw new \InvalidArgumentException(sprintf('The option \'%s\' does not exist.', $name));
332
            }
333
334
            $mergedOptions[$name] = $value;
335
        }
336
337
        return $mergedOptions;
338
    }
339
340
    /**
341
     * Checks the specified output
342
     *
343
     * @param string $output  The output filename
344
     * @param string $command The generation command
345
     *
346
     * @throws \RuntimeException if the output file generation failed
347
     */
348
    protected function checkOutput($output, $command)
349
    {
350
        // the output file must exist
351
        if (!$this->fileExists($output)) {
352
            throw new \RuntimeException(sprintf(
353
                'The file \'%s\' was not created (command: %s).',
354
                $output, $command
355
            ));
356
        }
357
358
        // the output file must not be empty
359
        if (0 === $this->filesize($output)) {
360
            throw new \RuntimeException(sprintf(
361
                'The file \'%s\' was created but is empty (command: %s).',
362
                $output, $command
363
            ));
364
        }
365
    }
366
367
    /**
368
     * Checks the process return status
369
     *
370
     * @param int    $status  The exit status code
371
     * @param string $stdout  The stdout content
372
     * @param string $stderr  The stderr content
373
     * @param string $command The run command
374
     *
375
     * @throws \RuntimeException if the output file generation failed
376
     */
377
    protected function checkProcessStatus($status, $stdout, $stderr, $command)
378
    {
379
        if (0 !== $status and '' !== $stderr) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
380
            throw new \RuntimeException(sprintf(
381
                'The exit status code \'%s\' says something went wrong:'."\n"
382
                .'stderr: "%s"'."\n"
383
                .'stdout: "%s"'."\n"
384
                .'command: %s.',
385
                $status, $stderr, $stdout, $command
386
            ));
387
        }
388
    }
389
390
    /**
391
     * Creates a temporary file.
392
     * The file is not created if the $content argument is null
393
     *
394
     * @param string $content   Optional content for the temporary file
395
     * @param string $extension An optional extension for the filename
396
     *
397
     * @return string The filename
398
     */
399
    protected function createTemporaryFile($content = null, $extension = null)
400
    {
401
        $dir = rtrim($this->getTemporaryFolder(), DIRECTORY_SEPARATOR);
402
403
        if (!is_dir($dir)) {
404
            if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
405
                throw new \RuntimeException(sprintf("Unable to create directory: %s\n", $dir));
406
            }
407
        } elseif (!is_writable($dir)) {
408
            throw new \RuntimeException(sprintf("Unable to write in directory: %s\n", $dir));
409
        }
410
411
        $filename = $dir . DIRECTORY_SEPARATOR . uniqid('knp_snappy', true);
412
413
        if (null !== $extension) {
414
            $filename .= '.'.$extension;
415
        }
416
417
        if (null !== $content) {
418
            file_put_contents($filename, $content);
419
        }
420
421
        $this->temporaryFiles[] = $filename;
422
423
        return $filename;
424
    }
425
426
    /**
427
     * Removes all temporary files
428
     */
429
    public function removeTemporaryFiles()
430
    {
431
        foreach ($this->temporaryFiles as $file) {
432
            $this->unlink($file);
433
        }
434
    }
435
436
    /**
437
     * Builds the command string
438
     *
439
     * @param string       $binary  The binary path/name
440
     * @param string/array $input   Url(s) or file location(s) of the page(s) to process
0 ignored issues
show
Documentation introduced by
The doc-type string/array could not be parsed: Unknown type name "string/array" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
441
     * @param string       $output  File location to the image-to-be
442
     * @param array        $options An array of options
443
     *
444
     * @return string
445
     */
446
    protected function buildCommand($binary, $input, $output, array $options = [])
447
    {
448
        $command = $binary;
449
        $escapedBinary = escapeshellarg($binary);
450
        if (is_executable($escapedBinary)) {
451
            $command = $escapedBinary;
452
        }
453
454
        foreach ($options as $key => $option) {
455
            if (null !== $option && false !== $option) {
456
457
                if (true === $option) {
458
                    // Dont't put '--' if option is 'toc'.
459
                    if ($key == 'toc') {
460
                        $command .= ' '.$key;
461
                    } else {
462
                        $command .= ' --'.$key;
463
                    }
464
465
                } elseif (is_array($option)) {
466
                    if ($this->isAssociativeArray($option)) {
467
                        foreach ($option as $k => $v) {
468
                            $command .= ' --'.$key.' '.escapeshellarg($k).' '.escapeshellarg($v);
469
                        }
470
                    } else {
471
                        foreach ($option as $v) {
472
                            $command .= ' --'.$key.' '.escapeshellarg($v);
473
                        }
474
                    }
475
476
                } else {
477
                    // Dont't add '--' if option is "cover"  or "toc".
478
                    if (in_array($key, ['toc', 'cover'])) {
479
                        $command .= ' '.$key.' '.escapeshellarg($option);
480
                    } elseif (in_array($key, ['image-dpi', 'image-quality'])) {
481
                        $command .= ' --'.$key.' '. (int) $option;
482
                    } else {
483
                        $command .= ' --'.$key.' '.escapeshellarg($option);
484
                    }
485
                }
486
            }
487
        }
488
489
        if (is_array($input)) {
490
            foreach ($input as $i) {
491
                $command .= ' '.escapeshellarg($i).' ';
492
            }
493
            $command .= escapeshellarg($output);
494
        } else {
495
            $command .= ' '.escapeshellarg($input).' '.escapeshellarg($output);
496
        }
497
498
        return $command;
499
    }
500
501
    /**
502
     * Return true if the array is an associative array
503
     * and not an indexed array
504
     *
505
     * @param array $array
506
     *
507
     * @return boolean
508
     */
509
    protected function isAssociativeArray(array $array)
510
    {
511
        return (bool) count(array_filter(array_keys($array), 'is_string'));
512
    }
513
514
    /**
515
     * Executes the given command via shell and returns the complete output as
516
     * a string
517
     *
518
     * @param string $command
519
     *
520
     * @return array(status, stdout, stderr)
0 ignored issues
show
Documentation introduced by
The doc-type array(status, could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
521
     */
522
    protected function executeCommand($command)
523
    {
524
        $process = new Process($command, null, $this->env);
525
526
        if (false !== $this->timeout) {
527
            $process->setTimeout($this->timeout);
0 ignored issues
show
Documentation introduced by
$this->timeout is of type boolean, but the function expects a integer|double|null.

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...
528
        }
529
530
        $process->run();
531
532
        return [
533
            $process->getExitCode(),
534
            $process->getOutput(),
535
            $process->getErrorOutput(),
536
        ];
537
    }
538
539
    /**
540
     * Prepares the specified output
541
     *
542
     * @param string  $filename  The output filename
543
     * @param boolean $overwrite Whether to overwrite the file if it already
544
     *                           exist
545
     *
546
     * @throws Exception\FileAlreadyExistsException
547
     * @throws \RuntimeException
548
     * @throws \InvalidArgumentException
549
     */
550
    protected function prepareOutput($filename, $overwrite)
551
    {
552
        $directory = dirname($filename);
553
554
        if ($this->fileExists($filename)) {
555
            if (!$this->isFile($filename)) {
556
                throw new \InvalidArgumentException(sprintf(
557
                    'The output file \'%s\' already exists and it is a %s.',
558
                    $filename, $this->isDir($filename) ? 'directory' : 'link'
559
                ));
560
            } elseif (false === $overwrite) {
561
                throw new Exceptions\FileAlreadyExistsException(sprintf(
562
                    'The output file \'%s\' already exists.',
563
                    $filename
564
                ));
565
            } elseif (!$this->unlink($filename)) {
566
                throw new \RuntimeException(sprintf(
567
                    'Could not delete already existing output file \'%s\'.',
568
                    $filename
569
                ));
570
            }
571
        } elseif (!$this->isDir($directory) && !$this->mkdir($directory)) {
572
            throw new \RuntimeException(sprintf(
573
                'The output file\'s directory \'%s\' could not be created.',
574
                $directory
575
            ));
576
        }
577
    }
578
579
    /**
580
     * Get TemporaryFolder
581
     *
582
     * @return string
583
     */
584
    public function getTemporaryFolder()
585
    {
586
        if ($this->temporaryFolder === null) {
587
            return sys_get_temp_dir();
588
        }
589
590
        return $this->temporaryFolder;
591
    }
592
593
    /**
594
     * Set temporaryFolder
595
     *
596
     * @param string $temporaryFolder
597
     *
598
     * @return $this
599
     */
600
    public function setTemporaryFolder($temporaryFolder)
601
    {
602
        $this->temporaryFolder = $temporaryFolder;
603
604
        return $this;
605
    }
606
607
    /**
608
     * Wrapper for the "file_get_contents" function
609
     *
610
     * @param string $filename
611
     *
612
     * @return string
613
     */
614
    protected function getFileContents($filename)
615
    {
616
        return file_get_contents($filename);
617
    }
618
619
    /**
620
     * Wrapper for the "file_exists" function
621
     *
622
     * @param string $filename
623
     *
624
     * @return boolean
625
     */
626
    protected function fileExists($filename)
627
    {
628
        return file_exists($filename);
629
    }
630
631
    /**
632
     * Wrapper for the "is_file" method
633
     *
634
     * @param string $filename
635
     *
636
     * @return boolean
637
     */
638
    protected function isFile($filename)
639
    {
640
        return strlen($filename) <= PHP_MAXPATHLEN && is_file($filename);
641
    }
642
643
    /**
644
     * Wrapper for the "filesize" function
645
     *
646
     * @param string $filename
647
     *
648
     * @return integer or FALSE on failure
649
     */
650
    protected function filesize($filename)
651
    {
652
        return filesize($filename);
653
    }
654
655
    /**
656
     * Wrapper for the "unlink" function
657
     *
658
     * @param string $filename
659
     *
660
     * @return boolean
661
     */
662
    protected function unlink($filename)
663
    {
664
        return $this->fileExists($filename) ? unlink($filename) : false;
665
    }
666
667
    /**
668
     * Wrapper for the "is_dir" function
669
     *
670
     * @param string $filename
671
     *
672
     * @return boolean
673
     */
674
    protected function isDir($filename)
675
    {
676
        return is_dir($filename);
677
    }
678
679
    /**
680
     * Wrapper for the mkdir function
681
     *
682
     * @param string $pathname
683
     *
684
     * @return boolean
685
     */
686
    protected function mkdir($pathname)
687
    {
688
        return mkdir($pathname, 0777, true);
689
    }
690
}
691