vendor/knplabs/knp-snappy/src/Knp/Snappy/AbstractGenerator.php line 68

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