Commit 1d7eb74d authored by Jacob Rockowitz's avatar Jacob Rockowitz
Browse files

Issue #3241469 by tannguyenhn, jrockowitz: Control the file system used by the signature element

parent 536650bc
Loading
Loading
Loading
Loading
+10 −2
Original line number Diff line number Diff line
@@ -28,6 +28,14 @@ elements: |
    '#type': webform_image_file
    '#title': image_file
    '#file_extensions': gif
  signature_public:
    '#type': webform_signature
    '#title': signature_public
    '#uri_scheme': public
  signature_private:
    '#type': webform_signature
    '#title': signature_private
    '#uri_scheme': private

css: '/** custom webform css **/'
javascript: ''
+8 −0
Original line number Diff line number Diff line
@@ -78,6 +78,14 @@ body {
    $image_style_token_query = [WEBFORM_ENTITY_PRINT_IMAGE_TOKEN => _webform_entity_print_token_generate($image_style_uri)];
    $assert_session->responseContains('&' . UrlHelper::buildQuery($image_style_token_query));

    // Check signature private image.
    $this->assertRaw('<label>signature_private</label>');
    $this->assertRaw("/webform/test_entity_print/signature_private/$sid/signature-");

    // Check signature public image.
    $this->assertRaw('<label>signature_public</label>');
    $this->assertRaw("/webform/test_entity_print/signature_public/$sid/signature-");

    // Check image access.
    $this->drupalLogout();
    $this->drupalGet($image_uri);
+63 −1
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
@@ -16,6 +17,7 @@ use Drupal\Core\Access\AccessResult;
use Drupal\Core\Url;
use Drupal\Core\Render\Markup;
use Drupal\Core\Session\AccountInterface;
use Drupal\webform\Entity\Webform;
use Drupal\webform\WebformSubmissionInterface;

/**
@@ -55,6 +57,50 @@ function webform_entity_print_webform_submission_access(WebformSubmissionInterfa
  return AccessResult::allowedIf($webform_entity_print_token === $encrypt_token);
}

/**
 * Implements hook_file_download().
 */
function webform_entity_print_file_download($uri) {
  if (!preg_match('#/webform/([^/]+)/#', $uri, $match)) {
    return NULL;
  }

  $webform_id = $match[1];
  $webform = Webform::load($webform_id);
  if (!$webform) {
    return NULL;
  }

  // Get signature elements.
  $signature_elements = [];
  $elements = $webform->getElementsDecodedAndFlattened();
  foreach ($elements as $element_key => $element) {
    if (isset($element['#type']) && $element['#type'] === 'webform_signature') {
      $signature_elements[] = $element_key;
    }
  }

  // Match signature element.
  if ($signature_elements && preg_match_all('#/webform/' . $webform_id . '/(' . implode('|', $signature_elements) . ')/#', $uri, $matches)) {
    $webform_entity_print_token = \Drupal::request()->get('webform_entity_print_itok');
    $encrypt_token = _webform_entity_print_token_generate(file_create_url($uri));
    if ($webform_entity_print_token === $encrypt_token) {
      /** @var \Drupal\Core\File\FileSystemInterface $file_system */
      $file_system = \Drupal::service('file_system');
      $filename = $file_system->basename($uri);
      $filesize = filesize($file_system->realpath($uri));
      return [
        'Content-Type' => 'image/png',
        'Content-Length' => $filesize,
        'Cache-Control' => 'private',
        'Content-Disposition' => 'inline; filename="' . Unicode::mimeHeaderEncode($filename) . '"',
      ];
    }
  }

  return NULL;
}

/**
 * Implements hook_ENTITY_TYPE_view_alter() for webform_submission entities.
 *
@@ -289,7 +335,7 @@ function webform_entity_print_preprocess_entity_print(array &$variables) {
  // Render the webform submission data.
  $html = (string) \Drupal::service('renderer')->render($content);

  // Only matching <img src=""/>.
  // Only matching <img src=""/webform/sid/*>.
  if (preg_match_all('#(src\s*=\s*")([^"]+(?:/private|/system/files)/webform/' . $webform_id . '/' . $sid . '/[^"]+)#', $html, $matches)) {
    foreach ($matches[2] as $index => $found_uri) {
      $token_query = [WEBFORM_ENTITY_PRINT_IMAGE_TOKEN => _webform_entity_print_token_generate($found_uri)];
@@ -298,6 +344,22 @@ function webform_entity_print_preprocess_entity_print(array &$variables) {
    }
  }

  // Only matching <img src=""/webform/signature_element_key/>.
  $elements = $webform->getElementsDecodedAndFlattened();
  $signature_elements = [];
  foreach ($elements as $element_key => $element) {
    if (isset($element['#type']) && $element['#type'] === 'webform_signature') {
      $signature_elements[] = $element_key;
    }
  }
  if ($signature_elements && preg_match_all('#(src\s*=\s*")([^"]+(?:/private|/system/files)/webform/' . $webform_id . '/(?:' . implode('|', $signature_elements) . ')/[^"]+)#', $html, $matches)) {
    foreach ($matches[2] as $index => $found_uri) {
      $token_query = [WEBFORM_ENTITY_PRINT_IMAGE_TOKEN => _webform_entity_print_token_generate($found_uri)];
      $replace_uri = $found_uri . (strpos($found_uri, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($token_query);
      $html = str_replace($matches[0][$index], $matches[1][$index] . $replace_uri, $html);
    }
  }

  // The HTML markup is safe because it has already been rendered.
  $content = [
    '#markup' => Markup::create($html),
+11 −14
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ use Drupal\file\FileInterface;
use Drupal\webform\Element\WebformHtmlEditor;
use Drupal\webform\Entity\WebformSubmission;
use Drupal\webform\Plugin\WebformElementAttachmentInterface;
use Drupal\webform\Plugin\WebformElementFileDownloadAccessInterface;
use Drupal\webform\Plugin\WebformElementBase;
use Drupal\webform\Utility\WebformOptionsHelper;
use Drupal\webform\WebformInterface;
@@ -31,7 +32,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
 * Provides a base class webform 'managed_file' elements.
 */
abstract class WebformManagedFileBase extends WebformElementBase implements WebformElementAttachmentInterface, WebformElementEntityReferenceInterface {
abstract class WebformManagedFileBase extends WebformElementBase implements WebformElementAttachmentInterface, WebformElementEntityReferenceInterface, WebformElementFileDownloadAccessInterface {

  /**
   * List of blacklisted mime types that must be downloaded.
@@ -1339,20 +1340,14 @@ abstract class WebformManagedFileBase extends WebformElementBase implements Webf
  }

  /**
   * Control access to webform submission private file downloads.
   *
   * @param string $uri
   *   The URI of the file.
   *
   * @return mixed
   *   Returns NULL if the file is not attached to a webform submission.
   *   Returns -1 if the user does not have permission to access a webform.
   *   Returns an associative array of headers.
   *
   * @see hook_file_download()
   * @see webform_file_download()
   * {@inheritdoc}
   */
  public static function accessFileDownload($uri) {
    // Make sure the file.module is installed.
    if (!\Drupal::moduleHandler()->moduleExists('file')) {
      return NULL;
    }

    $files = \Drupal::entityTypeManager()
      ->getStorage('file')
      ->loadByProperties(['uri' => $uri]);
@@ -1395,7 +1390,9 @@ abstract class WebformManagedFileBase extends WebformElementBase implements Webf
   *   An associative array of visible stream wrappers keyed by type.
   */
  public static function getVisibleStreamWrappers() {
    $stream_wrappers = \Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE);
    /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
    $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
    $stream_wrappers = $stream_wrapper_manager->getNames(StreamWrapperInterface::WRITE_VISIBLE);
    if (!\Drupal::config('webform.settings')->get('file.file_public')) {
      unset($stream_wrappers['public']);
    }
+110 −10
Original line number Diff line number Diff line
@@ -3,11 +3,16 @@
namespace Drupal\webform\Plugin\WebformElement;

use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\webform\Element\WebformSignature as WebformSignatureElement;
use Drupal\webform\Entity\Webform;
use Drupal\webform\Entity\WebformSubmission;
use Drupal\webform\Plugin\WebformElementBase;
use Drupal\webform\Plugin\WebformElementFileDownloadAccessInterface;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -22,7 +27,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 *   category = @Translation("Advanced elements"),
 * )
 */
class WebformSignature extends WebformElementBase {
class WebformSignature extends WebformElementBase implements WebformElementFileDownloadAccessInterface {

  /**
   * The file system service.
@@ -48,6 +53,7 @@ class WebformSignature extends WebformElementBase {
      // General settings.
      'description' => $this->t('Sign above'),
      'readonly' => FALSE,
      'uri_scheme' => 'public',
    ] + parent::defineDefaultProperties();
    unset(
      $properties['format_items'],
@@ -196,13 +202,23 @@ class WebformSignature extends WebformElementBase {
    // Warn people about saving signatures when saving of results is disabled.
    /** @var \Drupal\webform\WebformInterface $webform */
    $webform = $form_state->getFormObject()->getWebform();
    if ($webform->isResultsDisabled()) {
      $image_directory = 'public://webform/' . $webform->id() . '/{element_key}';
    $form['signature'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Signature settings'),
      '#access' => TRUE,
    ];
    $scheme_options = static::getVisibleStreamWrappers();
    $uri_scheme = $this->getDefaultProperty('uri_scheme');
    $image_directory = $uri_scheme . '://webform/' . $webform->id() . '/{element_key}';
    $form['signature']['uri_scheme'] = [
      '#type' => 'radios',
      '#title' => $this->t('Signature upload destination'),
      '#description' => $this->t('Select where the final Signatures should be stored. Both public and private storage store the signature using a secure hash as the file name. Public files should be adequate for most use cases. Private storage has more overhead than public files, but allows restricted access to files within this element.'),
      '#options' => $scheme_options,
      '#access' => TRUE,
      '#required' => TRUE,
    ];
    if ($webform->isResultsDisabled()) {
      $form['signature']['signature_message'] = [
        '#type' => 'webform_message',
        '#message_message' => '<strong>' . $this->t('Saving of results is disabled.') . '</strong> ' .
@@ -224,7 +240,8 @@ class WebformSignature extends WebformElementBase {
    $sid = $webform_submission->id();

    // Delete signature image submission directory.
    $image_base_directory = 'public://webform/' . $webform->id();
    $uri_scheme = $this->getElementProperty($element, 'uri_scheme');
    $image_base_directory = $uri_scheme . '://webform/' . $webform->id();
    $image_directory = "$image_base_directory/$element_key/$sid";
    if (file_exists($image_directory)) {
      $this->fileSystem->deleteRecursive($image_directory);
@@ -242,7 +259,7 @@ class WebformSignature extends WebformElementBase {
  /**
   * Get a signature element's image URL.
   *
   * Signature image uses the public: file system because the image name is
   * Signature image uses the public|private file system and the image name is
   * a secure hash and there is no risk of https://www.drupal.org/psa-2016-003.
   *
   * @param array $element
@@ -274,7 +291,8 @@ class WebformSignature extends WebformElementBase {
      : $element['#webform_key'];
    $sid = $webform_submission->id();

    $image_base_directory = 'public://webform/' . $webform->id();
    $uri_scheme = $this->getElementProperty($element, 'uri_scheme');
    $image_base_directory = $uri_scheme . '://webform/' . $webform->id();

    // Create signature image (no results) directory.
    $image_signature_directory = "$image_base_directory/$element_key";
@@ -316,4 +334,86 @@ class WebformSignature extends WebformElementBase {
    return file_create_url($image_uri);
  }

  /**
   * Get visible stream wrappers.
   *
   * @return array
   *   An associative array of visible stream wrappers keyed by type.
   */
  public static function getVisibleStreamWrappers() {
    /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
    $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
    return $stream_wrapper_manager->getNames(StreamWrapperInterface::WRITE_VISIBLE);
  }

  /**
   * {@inheritdoc}
   */
  public static function accessFileDownload($uri) {
    // Check if signature file.
    // URI patterns
    // - private://webform/[webform_id]/[element_key]/[sid]/signature-[hash].png.
    // - private://webform/[webform_id]/[element_key]/signature-[hash].png.
    // @see WebformSignature::getImageUrl().
    /** @var \Drupal\Core\StreamWrapper\StreamWrapperManager $stream_wrapper_manager */
    $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
    $uri_target = $stream_wrapper_manager->getTarget($uri);
    if (!preg_match("/^webform\/(.*?)\/(.*?)\/(?:(.*?)\/)?signature-.*\.png$/", $uri_target, $matches)) {
      return NULL;
    }

    $webform_id = $matches[1];
    $element_key = $matches[2];
    $submission_id = (isset($matches[3])) ? $matches[3] : NULL;

    // Load webform and make sure it exist.
    /** @var \Drupal\webform\Entity\Webform $webform */
    $webform = Webform::load($webform_id);
    if (!$webform) {
      return NULL;
    }

    // Load element and make sure it is a signature.
    $webform_element = $webform->getElement($element_key);
    if ($webform_element['#type'] !== 'webform_signature') {
      return NULL;
    }

    $access = NULL;

    if ($submission_id) {
      // Check submission view access.
      $webform_submission = WebformSubmission::load($submission_id);
      if ($webform_submission && $webform_submission->access('view')) {
        $access = TRUE;
      }
    }
    else {
      // Check view any submission access.
      if ($webform->access('submission_view_any')) {
        $access = TRUE;
      }
    }

    if ($access === TRUE) {
      // Return file content headers.
      /** @var \Drupal\Core\File\FileSystemInterface $file_system */
      $file_system = \Drupal::service('file_system');
      $filename = $file_system->basename($uri);
      $filesize = filesize($file_system->realpath($uri));
      return [
        'Content-Type' => 'image/png',
        'Content-Length' => $filesize,
        'Cache-Control' => 'private',
        'Content-Disposition' => 'inline; filename="' . Unicode::mimeHeaderEncode($filename) . '"',
      ];
    }
    elseif ($access === FALSE) {
      return -1;
    }
    else {
      return NULL;
    }
  }

}
Loading