Completed
Pull Request — master (#256)
by
unknown
02:31
created

AbstractGenerator::isDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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