<?php

/**
 * @file
 * Provides the ability to export and import submissions.
 */

use Drupal\Component\Utility\Environment;
use Drupal\Core\File\Event\FileUploadSanitizeNameEvent;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\Entity\File;

/**
 * Implements hook_webform_help_info().
 */
function webform_submission_export_import_webform_help_info() {
  $help = [];
  $help['webform_submission_export_import'] = [
    'group' => 'forms',
    'title' => t('Upload'),
    'content' => t('The <strong>Upload</strong> page allows a CSV (comma separated values) file or URL to be uploaded, converted, and imported into webform submissions.'),
    'video_id' => 'import',
    'routes' => [
      // @see /admin/structure/webform/manage/{webform}/results/upload
      'entity.webform_submission_export_import.results_import',
      // @see /node/{node}/webform/results/upload
      'entity.node.webform_submission_export_import.results_import',
    ],
  ];
  return $help;
}

/**
 * Implements hook_local_tasks_alter().
 */
function webform_submission_export_import_local_tasks_alter(&$local_tasks) {
  // Remove webform node results import if the webform_node.module
  // is not installed.
  if (!\Drupal::moduleHandler()->moduleExists('webform_node')) {
    unset(
      $local_tasks['entity.node.webform_submission_export_import.results_import']
    );
  }
}

/* ************************************************************************** */
// The below code is copy of _file_save_upload_single() which allows imported
// files to be securely created.
/* ************************************************************************** */

/**
 * Saves a file upload to a new location.
 *
 * @param \SplFileInfo $file_info
 *   The file upload to save.
 * @param string $form_field_name
 *   A string that is the associative array key of the upload form element in
 *   the form array.
 * @param array $validators
 *   (optional) An associative array of callback functions used to validate the
 *   file.
 * @param bool $destination
 *   (optional) A string containing the URI that the file should be copied to.
 * @param int $replace
 *   (optional) The replace behavior when the destination file already exists.
 *
 * @return \Drupal\file\FileInterface|false
 *   The created file entity or FALSE if the uploaded file not saved.
 *
 * @throws \Drupal\Core\Entity\EntityStorageException
 *
 * @internal
 *   This method should only be called from file_save_upload(). Use that method
 *   instead.
 *
 * @see file_save_upload()
 * @see file_save_upload_single()
 */
function _webform_submission_export_import_file_save_upload_single(\SplFileInfo $file_info, $form_field_name, array $validators = [], $destination = FALSE, $replace = FileSystemInterface::EXISTS_RENAME) {
  /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  $file_system = \Drupal::service('file_system');
  $current_user = \Drupal::currentUser();

  /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');

  // Check for file upload errors and return FALSE for this file if a lower
  // level system error occurred. For a complete list of errors:
  // See http://php.net/manual/features.file-upload.errors.php.
  switch ($file_info->getError()) {
    case UPLOAD_ERR_INI_SIZE:
    case UPLOAD_ERR_FORM_SIZE:
      \Drupal::messenger()->addError(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', ['%file' => $file_info->getFilename(), '%maxsize' => format_size(Environment::getUploadMaxSize())]));
      return FALSE;

    case UPLOAD_ERR_PARTIAL:
    case UPLOAD_ERR_NO_FILE:
      \Drupal::messenger()->addError(t('The file %file could not be saved because the upload did not complete.', ['%file' => $file_info->getFilename()]));
      return FALSE;

    case UPLOAD_ERR_OK:
      // phpcs:disable
      /* ******************************************************************** */
      // DO NOT USE IF UPLOADED FILE.
      /* ******************************************************************** */
      // Final check that this is a valid upload, if it isn't, use the
      // default error handler.
      // if (is_uploaded_file($file_info->getRealPath())) {
      //   break;
      // }
      // phpcs:enable
      break;

    default:
      // Unknown error.
      \Drupal::messenger()->addError(t('The file %file could not be saved. An unknown error has occurred.', ['%file' => $file_info->getFilename()]));
      return FALSE;

  }

  // Begin building file entity.
  $values = [
    'uid' => $current_user->id(),
    'status' => 0,
    'filename' => $file_info->getClientOriginalName(),
    'uri' => $file_info->getRealPath(),
    'filesize' => $file_info->getSize(),
  ];
  $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guessMimeType($values['filename']);
  $file = File::create($values);

  $extensions = '';
  if (isset($validators['file_validate_extensions'])) {
    if (isset($validators['file_validate_extensions'][0])) {
      // Build the list of non-munged extensions if the caller provided them.
      $extensions = $validators['file_validate_extensions'][0];
    }
    else {
      // If 'file_validate_extensions' is set and the list is empty then the
      // caller wants to allow any extension. In this case we have to remove the
      // validator or else it will reject all extensions.
      unset($validators['file_validate_extensions']);
    }
  }
  else {
    // No validator was provided, so add one using the default list.
    // Build a default non-munged safe list for file_munge_filename().
    $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
    $validators['file_validate_extensions'] = [];
    $validators['file_validate_extensions'][0] = $extensions;
  }

  if (!empty($extensions)) {
    // Munge the filename to protect against possible malicious extension
    // hiding within an unknown file type (ie: filename.html.foo).
    $event = new FileUploadSanitizeNameEvent($file->getFilename(), $extensions);
    \Drupal::service('event_dispatcher')->dispatch($event);
    $file->setFilename($event->getFilename());
  }

  // Rename potentially executable files, to help prevent exploits (i.e. will
  // rename filename.php.foo and filename.php to filename.php.foo.txt and
  // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
  // evaluates to TRUE.
  if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FileSystemInterface::INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) !== '.txt')) {
    $file->setMimeType('text/plain');
    // The destination filename will also later be used to create the URI.
    $file->setFilename($file->getFilename() . '.txt');
    // The .txt extension may not be in the allowed list of extensions. We have
    // to add it here or else the file upload will fail.
    if (!empty($extensions)) {
      $validators['file_validate_extensions'][0] .= ' txt';
      \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
    }
  }

  // If the destination is not provided, use the temporary directory.
  if (empty($destination)) {
    $destination = 'temporary://';
  }

  // Assert that the destination contains a valid stream.
  $destination_scheme = $stream_wrapper_manager->getScheme($destination);
  if (!$stream_wrapper_manager->isValidScheme($destination_scheme)) {
    \Drupal::messenger()->addError(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]));
    return FALSE;
  }

  $file->source = $form_field_name;
  // A file URI may already have a trailing slash or look like "public://".
  if (substr($destination, -1) !== '/') {
    $destination .= '/';
  }
  $file->destination = \Drupal::service('file_system')->getDestinationFilename($destination . $file->getFilename(), $replace);
  // If \Drupal::service('file_system')->getDestinationFilename() returns FALSE then $replace === FileSystemInterface::EXISTS_ERROR and
  // there's an existing file so we need to bail.
  if ($file->destination === FALSE) {
    \Drupal::messenger()->addError(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', ['%source' => $form_field_name, '%directory' => $destination]));
    return FALSE;
  }

  // Add in our check of the file name length.
  $validators['file_validate_name_length'] = [];

  // Call the validation functions specified by this function's caller.
  $errors = file_validate($file, $validators);

  // Check for errors.
  if (!empty($errors)) {
    $message = [
      'error' => [
        '#markup' => t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
      ],
      'item_list' => [
        '#theme' => 'item_list',
        '#items' => $errors,
      ],
    ];
    // @todo Add support for render arrays in
    // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
    // @see https://www.drupal.org/node/2505497.
    \Drupal::messenger()->addError(\Drupal::service('renderer')->renderPlain($message));
    return FALSE;
  }

  $file->setFileUri($file->destination);

  /* ******************************************************************** */
  // DO NOT USE MOVE UPLOADED FILE.
  /* ******************************************************************** */
  // phpcs:ignore
  // if (!drupal_move_uploaded_file($file_info->getRealPath(), $file->getFileUri())) {
  if (!\Drupal::service('file_system')->move($file_info->getRealPath(), $file->getFileUri(), $replace)) {
    \Drupal::messenger()->addError(t('File upload error. Could not move uploaded file.'));
    \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
    return FALSE;
  }

  // Set the permissions on the new file.
  $file_system->chmod($file->getFileUri());

  // If we are replacing an existing file re-use its database record.
  // @todo Do not create a new entity in order to update it.
  // @see https://www.drupal.org/node/2241865.
  if ($replace === FileSystemInterface::EXISTS_REPLACE) {
    $existing_files = \Drupal::entityTypeManager()
      ->getStorage('file')
      ->loadByProperties(['uri' => $file->getFileUri()]);
    if (count($existing_files)) {
      $existing = reset($existing_files);
      $file->fid = $existing->id();
      $file->setOriginalId($existing->id());
    }
  }

  // If we made it this far it's safe to record this file in the database.
  $file->save();

  // Allow an anonymous user who creates a non-public file to see it. See
  // \Drupal\file\FileAccessControlHandler::checkAccess().
  if ($current_user->isAnonymous() && $destination_scheme !== 'public') {
    $session = \Drupal::request()->getSession();
    $allowed_temp_files = $session->get('anonymous_allowed_file_ids', []);
    $allowed_temp_files[$file->id()] = $file->id();
    $session->set('anonymous_allowed_file_ids', $allowed_temp_files);
  }
  return $file;
}
