You are here

WkhtmltopdfController.php in wkhtmltopdf 2.0.x

Same filename and directory in other branches
  1. 8 src/Controller/WkhtmltopdfController.php

File

src/Controller/WkhtmltopdfController.php
View source
<?php

namespace Drupal\wkhtmltopdf\Controller;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Config\ConfigFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelInterface;

/**
 * A Controller to generate PDFs and return them as a binary reponse.
 */
class WkhtmltopdfController extends ControllerBase {

  /**
   * @var Drupal\Core\Config\ImmutableConfig
   *
   * The wkhtmltopdf module config
   */
  public $settings;

  /**
   * @var Symfony\Component\HttpFoundation\Request
   *
   * The current request object
   */
  public $request;

  /**
   * @var Drupal\Core\File\FileSystemInterface
   */
  public $filesystem;

  /**
   * @var Drupal\Core\Logger\LoggerChannelInterface
   *
   * The logger for the wkhtmltopdf module
   */
  public $logger;

  /**
   * {@inheritdoc}
   */
  public function __construct(ConfigFactory $config, Request $request, FileSystemInterface $filesystem, LoggerChannelInterface $logger) {
    $this->settings = $config
      ->get('wkhtmltopdf.settings');
    $this->request = $request;
    $this->filesystem = $filesystem;
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('config.factory'), $container
      ->get('request_stack')
      ->getCurrentRequest(), $container
      ->get('file_system'), $container
      ->get('logger.factory')
      ->get('wkhtmltopdf'));
  }

  /**
   * Generate pdf file.
   *
   * @return Symfony\Component\HttpFoundation\Response
   *   The generated PDF with an appropriate Content-Type header
   */
  public function generatePdf() {
    $url = $this->request->query
      ->get('url');
    $drupal_file_path = $this
      ->createFile($url);
    $filename = $this
      ->generateFilename($url);
    $file_response = $this
      ->getFileResponse($drupal_file_path, $filename);
    return $file_response;
  }

  /**
   * Creates a file response object to return to the client
   *
   * @param string $drupal_file_path
   *   The drupal filepath in public://
   *
   * @param string $filename
   *   The filename for http content disposition header
   *
   * @return Symfony\Component\HttpFoundation\BinaryFileResponse
   *   The file response to return to the client
   */
  protected function getFileResponse($drupal_file_path, $filename) {
    $force_download = $this->settings
      ->get('wkhtmltopdf_download');
    $headers = [
      'Content-Type' => 'application/pdf',
    ];
    if ($force_download) {
      $headers['Content-Disposition'] = 'attachment;filename="' . $filename . '"';
    }
    $fileResponse = new BinaryFileResponse($drupal_file_path, 200, $headers, true);

    // We don't want these generated files to hang around forever
    $fileResponse
      ->deleteFileAfterSend(TRUE);
    return $fileResponse;
  }

  /**
   * Actually sends the shell command, and returns after the pdf file has been created
   *
   * @param string $url
   *   The absolute url that we're generating a pdf of
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   *   If the wkhtmltopdf subdirectory can't be created in the public filesystem, just give up
   *
   * @return string
   *   The drupal filepath in public://
   */
  protected function createFile($url) {
    $wkhtmltopdf_directory = 'public://wkhtmltopdf';
    if (!$this->filesystem
      ->prepareDirectory($wkhtmltopdf_directory, FileSystemInterface::CREATE_DIRECTORY)) {

      // folder could not be created
      $this->logger
        ->error('wkhtmltopdf subdirectory could not be created in the public filesystem');
      throw new HttpException(500, 'An error occurred generating the pdf.');
    }

    // create a likely to be unique filename for generating the pdf
    $filename = uniqid('wkhtmltopdf_', TRUE) . '.pdf';
    $drupal_file_path = $wkhtmltopdf_directory . '/' . $filename;
    $command = $this
      ->buildShellCommand($url, $drupal_file_path);
    $lock = \Drupal::lock();
    if ($lock
      ->acquire(__FILE__) !== FALSE) {
      shell_exec($command);
      $lock
        ->release(__FILE__);
    }
    else {
      while ($lock
        ->acquire(__FILE__) === FALSE) {
        $lock
          ->wait(__FILE__, 3);
      }
      if ($lock
        ->acquire(__FILE__) !== FALSE) {
        shell_exec($command);
        $lock
          ->release(__FILE__);
      }
    }
    return $drupal_file_path;
  }

  /**
   * Builds the shell command including parameters
   *
   * @param string $url
   *   The absolute url that we're generating a pdf of
   *
   * @param string $drupal_file_path
   *   The drupal file path where the pdf will be generated
   *
   * @return string
   *   The full shell command including parameters
   */
  protected function buildShellCommand($url, $drupal_file_path) {
    $binary = $this->settings
      ->get('wkhtmltopdf_bin');
    if (empty($binary)) {
      $this->logger
        ->error('wkhtmltopdf binary path has not been set in the configuration');
      throw new HttpException(500, 'An error occurred generating the pdf.');
    }
    $physical_file_path = $this->filesystem
      ->realpath($drupal_file_path);
    $arguments = $this
      ->getCommandLineArguments();
    $command = $binary;
    foreach ($arguments as $argument => $argument_value) {
      if (empty($argument_value)) {
        $command .= ' --' . $argument;
      }
      else {
        $command .= ' --' . $argument . ' ' . escapeshellarg($argument_value);
      }
    }
    $command .= ' ' . $url . ' ' . $physical_file_path;
    return $command;
  }

  /**
   * Gets the command line arguments for the wkhtmltopdf shell command based on the settings
   *
   * @return
   *   An associative array where argument name is the array key, and the argument value is the array value.
   *   Arguments without values use an empty string for value
   */
  protected function getCommandLineArguments() {
    $arguments = [];
    if ($this->settings
      ->get('wkhtmltopdf_zoom')) {
      $arguments['zoom'] = $this->settings
        ->get('wkhtmltopdf_zoom');
    }
    if ($this->settings
      ->get('wkhtmltopdf_printmedia')) {
      $arguments['print-media-type'] = '';
    }
    $this
      ->validateArguments($arguments);
    return $arguments;
  }

  /**
   * Validates the command line arguments are valid
   *
   * @param array $arguments
   * An associative array where argument name is the array key, and the argument value is the array value.
   */
  private function validateArguments($arguments) {
    foreach ($arguments as $argument_name => $argument_value) {
      $this
        ->validateArgument($argument_name, $argument_value);
    }
  }
  private function validateArgument($argument_name, $argument_value) {
    $validArguments = [
      'allow',
      'background',
      'bypass-proxy-for',
      'cache-dir',
      'checkbox-checked-svg',
      'checkbox-svg',
      'collate',
      'cookie',
      'cookie-jar',
      'copies',
      'custom-header',
      'custom-header-propagation',
      'debug-javascript',
      'default-header',
      'disable-dotted-lines',
      'disable-external-links',
      'disable-forms',
      'disable-internal-links',
      'disable-javascript',
      'disable-local-file-access',
      'disable-plugins',
      'disable-smart-shrinking',
      'disable-toc-back-links',
      'disable-toc-links',
      'dpi',
      'dump-default-toc-xsl',
      'dump-outline',
      'enable-external-links',
      'enable-forms',
      'enable-internal-links',
      'enable-javascript',
      'enable-local-file-access',
      'enable-plugins',
      'enable-smart-shrinking',
      'enable-toc-back-links',
      'encoding',
      'exclude-from-outline',
      'extended-help',
      'footer-center',
      'footer-font-name',
      'footer-font-size',
      'footer-html',
      'footer-left',
      'footer-line',
      'footer-right',
      'footer-spacing',
      'grayscale',
      'header-center',
      'header-font-name',
      'header-font-size',
      'header-html',
      'header-left',
      'header-line',
      'header-right',
      'header-spacing',
      'help',
      'htmldoc',
      'image-dpi',
      'image-quality',
      'images',
      'include-in-outline',
      'javascript-delay',
      'keep-relative-links',
      'license',
      'load-error-handling',
      'load-media-error-handling',
      'log-level',
      'lowquality',
      'manpage',
      'margin-bottom',
      'margin-left',
      'margin-right',
      'margin-top',
      'minimum-font-size',
      'no-background',
      'no-collate',
      'no-custom-header-propagation',
      'no-debug-javascript',
      'no-footer-line',
      'no-header-line',
      'no-images',
      'no-outline',
      'no-pdf-compression',
      'no-print-media-type',
      'no-stop-slow-scripts',
      'orientation',
      'outline',
      'outline-depth',
      'page-height',
      'page-offset',
      'page-size',
      'page-width',
      'password',
      'post',
      'post-file',
      'print-media-type',
      'proxy',
      'proxy-hostname-lookup',
      'quiet',
      'radiobutton-checked-svg',
      'radiobutton-svg',
      'read-args-from-stdin',
      'readme',
      'replace',
      'resolve-relative-links',
      'run-script',
      'ssl-crt-path',
      'ssl-key-password',
      'ssl-key-path',
      'stop-slow-scripts',
      'title',
      'toc-header-text',
      'toc-level-indentation',
      'toc-text-size-shrink',
      'top',
      'username',
      'user-style-sheet',
      'use-xserver',
      'version',
      'viewport-size',
      'window-status',
      'xsl-style-sheet',
      'zoom',
    ];
    if (!in_array($argument_name, $validArguments, TRUE)) {
      $this->logger
        ->error('An invalid argument was passed to the wkhtmltopdf binary: %argument_name', [
        '%argument_name' => $argument_name,
      ]);
      throw new HttpException(500, 'An error occurred generating the pdf.');
    }
  }

  /**
   * Generates a filename based on the url
   *
   * @param string $url
   *   The absolute url that we're generating a pdf of
   *
   * @return string
   *   a generated filename that's no longer than 255 chars containing only alphanumeric characters
   */
  protected function generateFilename($url) {
    $filename = trim(preg_replace('/[^a-zA-Z0-9]+/', '_', $url), '_') . '.pdf';

    // filenames shouldn't be longer than 255 chars
    if (strlen($filename) > 255) {

      // take from the back to preserve extension
      // URLs tend to have their most important info at the end
      $filename = substr($filename, -255, 255);
    }
    return $filename;
  }

}

Classes

Namesort descending Description
WkhtmltopdfController A Controller to generate PDFs and return them as a binary reponse.