<?php

namespace Drupal\webform\Plugin\WebformElement;

use Drupal\Component\Utility\Crypt;
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;
use Symfony\Component\HttpFoundation\HeaderUtils;

/**
 * Provides a 'signature' element.
 *
 * @WebformElement(
 *   id = "webform_signature",
 *   label = @Translation("Signature"),
 *   description = @Translation("Provides a form element to collect electronic signatures from users. Signature support is provided by the <a href=""https://github.com/szimek/signature_pad"">Signature Pad</a> library."),
 *   category = @Translation("Advanced elements"),
 * )
 */
class WebformSignature extends WebformElementBase implements WebformElementFileDownloadAccessInterface {

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->fileSystem = $container->get('file_system');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  protected function defineDefaultProperties() {
    $properties = [
      // General settings.
      'description' => $this->t('Sign above'),
      'readonly' => FALSE,
      'uri_scheme' => 'public',
    ] + parent::defineDefaultProperties();
    unset(
      $properties['format_items'],
      $properties['format_items_html'],
      $properties['format_items_text']
    );
    return $properties;
  }

  /* ************************************************************************ */

  /**
   * {@inheritdoc}
   */
  public function prepare(array &$element, WebformSubmissionInterface $webform_submission = NULL) {
    if (empty($element['#description'])) {
      $element['#description'] = $this->t('Sign above');
      $element['#description_display'] = 'after';
    }
    parent::prepare($element, $webform_submission);
  }

  /**
   * {@inheritdoc}
   */
  protected function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    $value = $this->getValue($element, $webform_submission, $options);

    $format = $this->getItemFormat($element);

    switch ($format) {
      case 'image':
        if (empty($value)) {
          return '[' . $this->t('not signed') . ']';
        }

        $src = $this->getImageUrl($element, $webform_submission, $options);
        return $src ? [
          '#type' => 'html_tag',
          '#tag' => 'img',
          '#attributes' => [
            'src' => $src,
            'alt' => $this->t('Signature'),
            'class' => ['webform-signature-image'],
          ],
          '#attached' => ['library' => ['webform/webform.element.signature']],
        ] : '[' . $this->t('not valid') . ']';

      default:
        return parent::formatHtmlItem($element, $webform_submission, $options);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function formatTextItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    $format = $this->getItemFormat($element);
    switch ($format) {
      case 'image':
      case 'status':
        $value = $this->getValue($element, $webform_submission, $options);
        return ($value) ? '[' . $this->t('signed') . ']' : '[' . $this->t('not signed') . ']';

      case 'url':
        return $this->getImageUrl($element, $webform_submission, $options);
    }

    return parent::formatTextItem($element, $webform_submission, $options);
  }

  /**
   * {@inheritdoc}
   */
  public function getItemDefaultFormat() {
    return 'image';
  }

  /**
   * {@inheritdoc}
   */
  public function getItemFormats() {
    return [
      'raw' => $this->t('Raw value'),
      'status' => $this->t('Status'),
      'url' => $this->t('URL'),
      'image' => $this->t('Image'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getExportDefaultOptions() {
    return [
      'signature_format' => 'status',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildExportOptionsForm(array &$form, FormStateInterface $form_state, array $export_options) {
    parent::buildExportOptionsForm($form, $form_state, $export_options);
    if (isset($form['signature'])) {
      return;
    }

    $form['signature'] = [
      '#type' => 'details',
      '#title' => $this->t('Signature options'),
      '#open' => TRUE,
    ];
    $form['signature']['signature_format'] = [
      '#type' => 'radios',
      '#title' => $this->t('Signature format'),
      '#options' => [
        'image' => $this->t('Image, the signature\'s <a href=":href">Data URI</a>.', [':href' => 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs']),
        'status' => $this->t("Status, displays 'signed' or 'no signed'"),
      ],
      '#default_value' => $export_options['signature_format'],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildExportRecord(array $element, WebformSubmissionInterface $webform_submission, array $export_options) {
    $element['#format'] = ($export_options['signature_format'] === 'status') ? 'image' : 'raw';
    return [$this->formatText($element, $webform_submission, $export_options)];
  }

  /**
   * {@inheritdoc}
   */
  public function getTestValues(array $element, WebformInterface $webform, array $options = []) {
    return ['data:image/png;base64,[encoded data, 27816 characters]'];
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);

    // Warn people about saving signatures when saving of results is disabled.
    /** @var \Drupal\webform\WebformInterface $webform */
    $webform = $form_state->getFormObject()->getWebform();
    $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> ' .
          $this->t('Signatures will still be saved to %directory.', ['%directory' => $image_directory]),
        '#message_type' => 'warning',
        '#access' => TRUE,
      ];
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function postDelete(array &$element, WebformSubmissionInterface $webform_submission) {
    $webform = $webform_submission->getWebform();
    $element_key = $element['#webform_key'];
    $sid = $webform_submission->id();

    // Delete signature image submission directory.
    $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);
    }

    // Please node, the signature image (no results) directory is deleted when
    // the Webform is deleted.
    // @see \Drupal\webform\WebformEntityStorage::delete
  }

  /* ************************************************************************ */
  // Signature image helpers.
  /* ************************************************************************ */

  /**
   * Get a signature element's image URL.
   *
   * 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
   *   An element.
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $options
   *   An array of options.
   *
   * @return string
   *   A signature element's image URI.
   *
   * @see https://stackoverflow.com/questions/11511511/how-to-save-a-png-image-server-side-from-a-base64-data-string
   */
  protected function getImageUrl(array $element, WebformSubmissionInterface $webform_submission, array $options = []) {
    $value = $this->getValue($element, $webform_submission, $options);
    if (!$value) {
      return '';
    }

    // Make sure existing signature values are valid.
    if (!WebformSignatureElement::isSignatureValid($value)) {
      return '';
    }

    $webform = $webform_submission->getWebform();
    $element_key = (isset($element['#webform_composite_key']))
      ? $element['#webform_composite_key']
      : $element['#webform_key'];
    $sid = $webform_submission->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";
    $this->fileSystem->prepareDirectory($image_signature_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
    $image_directory = $image_signature_directory;

    // Create signature image submission directory.
    if ($sid) {
      $image_submission_directory = "$image_base_directory/$element_key/$sid";
      $this->fileSystem->prepareDirectory($image_submission_directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
      $image_directory = $image_submission_directory;
    }

    /** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
    $file_url_generator = \Drupal::service('file_url_generator');

    // If a signature file was already created and shared using an
    // unsafe image hash, then return it.
    $unsafe_image_hash = Crypt::hmacBase64($value, Settings::getHashSalt());
    $unsafe_image_uri = "$image_directory/signature-$unsafe_image_hash.png";
    if (file_exists($unsafe_image_uri)) {
      return $file_url_generator->generateAbsoluteString($unsafe_image_uri);
    }

    $image_hash = Crypt::hmacBase64('webform-signature-' . $value, Settings::getHashSalt());
    $image_uri = "$image_directory/signature-$image_hash.png";

    if (!file_exists($image_uri)) {
      // Copy existing file.
      if ($sid && file_exists("$image_signature_directory/signature-$image_hash.png")) {
        $this->fileSystem->move(
          "$image_signature_directory/signature-$image_hash.png",
          "$image_directory/signature-$image_hash.png"
        );
      }
      else {
        $data = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $value));
        file_put_contents($image_uri, $data);
      }
    }

    return $file_url_generator->generateAbsoluteString($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' => HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_INLINE, (string) $filename),
      ];
    }
    elseif ($access === FALSE) {
      return -1;
    }
    else {
      return NULL;
    }
  }

}
