Commit d4746869 authored by slashrsm's avatar slashrsm
Browse files

Merge pull request #5 from primsi/upload_save_service

by primsi: provide a service that saves dropzonejs uploads as file entities
parents 2d5245a3 7a8d52e2
services:
dropzonejs.upload_save:
class: Drupal\dropzonejs\DropzoneJsUploadSave
arguments: ['@entity.manager', '@file.mime_type.guesser', '@file_system', '@logger.factory', '@renderer', '@config.factory']
......@@ -167,6 +167,8 @@ class UploadController extends ControllerBase {
*
* @throws \Drupal\dropzonejs\UploadException
* @throws Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*
* @todo Handle all the possible upload errors. See file_save_upload().
*/
protected function handleUpload() {
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file */
......
<?php
/**
* @file
* Contains \Drupal\dropzonejs\DropzoneJsUploadSave.
*/
namespace Drupal\dropzonejs;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\file\FileInterface;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\Validator\Constraints\File;
use Drupal\Core\File\FileSystemInterface;
/**
* A service that saves files uploaded by the dropzonejs element as file
* entities.
*
* Most of this file mimics or directly copies what core does. For more
* information and comments see file_save_upload().
*/
class DropzoneJsUploadSave implements DropzoneJsUploadSaveInterface {
/**
* Entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Mime type guesser service.
*
* @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
*/
protected $mimeTypeGuesser;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface;
*/
protected $fileSystem;
/**
* The logger service.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $logger;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Construct the DropzoneUploadSave object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* Entity manager service.
* @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mimetype_guesser
* The mime type guesser service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* Config factory service.
*/
public function __construct(EntityManagerInterface $entity_manager, MimeTypeGuesserInterface $mimetype_guesser, FileSystemInterface $file_system, LoggerChannelFactoryInterface $logger_factory, RendererInterface $renderer, ConfigFactoryInterface $config_factory) {
$this->entityManager = $entity_manager;
$this->mimeTypeGuesser = $mimetype_guesser;
$this->fileSystem = $file_system;
$this->logger = $logger_factory->get('dropzonejs');
$this->renderer = $renderer;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function saveFile($uri, $destination, $extensions, AccountProxyInterface $user, $validators = []) {
// Create the file entity.
$file = $this->fileEntityFromUri($uri, $user);
// Handle potentialy dangerous extensions.
$renamed = $this->renameExecutableExtensions($file);
// 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 ($renamed && !empty($extensions)) {
$extensions .= ' txt';
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
}
// Validate the file.
$errors = $this->validateFile($file, $extensions, $validators);
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,
],
];
drupal_set_message($this->renderer->renderPlain($message), 'error');
return FALSE;
}
// Prepare destination.
if (!$this->prepareDestination($file, $destination)) {
drupal_set_message(t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]), 'error');
return FALSE;
}
// Move uploaded files from PHP's upload_tmp_dir to destination.
$move_result = file_unmanaged_move($uri, $file->getFileUri());
if (!$move_result) {
drupal_set_message(t('File upload error. Could not move uploaded file.'), 'error');
$this->logger->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.
$this->fileSystem->chmod($file->getFileUri());
// If we made it this far it's safe to record this file in the database.
$file->save();
return $file;
}
/**
* {@inheritdoc}
*/
public function fileEntityFromUri($uri, AccountProxyInterface $user) {
$uri = file_stream_wrapper_uri_normalize($uri);
$file_info = new \SplFileInfo($uri);
// Begin building file entity.
$values = [
'uid' => $user->id(),
'status' => 0,
'filename' => $file_info->getFilename(),
'uri' => $uri,
'filesize' => $file_info->getSize(),
'filemime' => $this->mimeTypeGuesser->guess($uri),
];
/** @var \Drupal\file\FileInterface $file */
$file = $this->entityManager->getStorage('file')->create($values);
return $file;
}
/**
* {@inheritdoc}
*/
public function validateFile(FileInterface $file, $extensions, array $additional_validators = []) {
$validators = $additional_validators;
if (!empty($extensions)) {
$validators['file_validate_extensions'] = [$extensions];
}
$validators['file_validate_name_length'] = [];
// Call the validation functions specified by this function's caller.
return file_validate($file, $validators);
}
/**
* Rename potentially executable files.
*
* @param \Drupal\file\FileInterface $file
* The file entity object.
*
* @return bool
* Whether the file was renamed or not.
*/
protected function renameExecutableExtensions(FileInterface $file) {
if (!$this->configFactory->get('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $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');
return TRUE;
}
return FALSE;
}
/**
* Validate and set destination the destination URI.
*
* @param \Drupal\file\FileInterface $file
* The file entity object.
* @param string $destination
* A string containing the URI that the file should be copied to. This must
* be a stream wrapper URI.
*
* @return bool
* True if the destination was sucesfully validated and set, otherwise
* false.
*/
protected function prepareDestination(FileInterface $file, $destination) {
// Assert that the destination contains a valid stream.
$destination_scheme = file_uri_scheme($destination);
if (!file_stream_wrapper_valid_scheme($destination_scheme)) {
return FALSE;
}
// Prepare the destination dir.
if (!file_exists($destination)) {
$this->fileSystem->mkdir($destination);
}
// A file URI may already have a trailing slash or look like "public://".
if (substr($destination, -1) != '/') {
$destination .= '/';
}
$file->destination = file_destination($destination . $file->getFilename(), FILE_EXISTS_RENAME);
$file->setFileUri($file->destination);
return TRUE;
}
}
<?php
/**
* @file
* Contains \Drupal\dropzonejs\DropzoneJsUploadSaveInterface.
*/
namespace Drupal\dropzonejs;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\file\FileInterface;
/**
* Provides an interface for classes that save DropzoneJs uploads.
*/
interface DropzoneJsUploadSaveInterface {
/**
* Save a uploaded file.
*
* Note: files beeing saved using this method are still flagged as temporary.
*
* @param string $uri
* The path to the file we want to upload.
* @param string $destination
* A string containing the URI that the file should be copied to. This must
* be a stream wrapper URI.
* @param string $extensions
* A space separated list of valid extensions.
* @param \Drupal\Core\Session\AccountProxyInterfac $user
* The owner of the file.
* @param array $validators
* An optional, associative array of callback functions used to validate the
* file. See file_validate() for more documentation. Note that we add
* file_validate_extensions and file_validate_name_length in this method
* already.
*
* @return \Drupal\file\FileInterface|bool
* The saved file entity of the newly created file entity or false if
* saving failed.
*/
public function saveFile($uri, $destination, $extensions, AccountProxyInterface $user, $validators = []);
/**
* Prepare a file entity from uri.
*
* @param string $uri
* File's uri.
* @param \Drupal\Core\Session\AccountProxyInterface $user
* The owner of the file.
*
* @return \Drupal\file\FileInterface
* A new entity file entity object, not saved yet.
*/
public function fileEntityFromUri($uri, AccountProxyInterface $user);
/**
* Validate the uploaded file.
*
* @param \Drupal\file\FileInterface $file
* The file entity object.
* @param array $extensions
* A space separated string of valid extensions.
* @param array $additional_validators
* An optional, associative array of callback functions used to validate the
* file. See file_validate() for more documentation. Note that we add
* file_validate_extensions and file_validate_name_length in this method
* already.
*
* @return array
* An array containing validation error messages.
*/
public function validateFile(FileInterface $file, $extensions, array $additional_validators = []);
}
......@@ -7,6 +7,7 @@
namespace Drupal\dropzonejs\Element;
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
......@@ -23,7 +24,8 @@ use Drupal\Core\Render\Element\FormElement;
* - #dropzone_description
* Will be visible inside the upload area.
* - #max_filesize
* Used by dropzonejs and expressed in MB. See
* Used by dropzonejs and expressed in number + unit (i.e. 1.1M) This will be
* converted to a form that DropzoneJs understands. See:
* http://www.dropzonejs.com/#config-maxFilesize
* - #extensions
* A string of valid extensions separated by a space.
......@@ -54,6 +56,7 @@ class DropzoneJs extends FormElement {
'#pre_render' => [[$class, 'preRenderDropzoneJs']],
'#theme' => 'dropzonejs',
'#theme_wrappers' => ['form_element'],
'#tree' => TRUE,
'#attached' => [
'library' => ['dropzonejs/dropzonejs', 'dropzonejs/integration']
],
......@@ -85,15 +88,18 @@ class DropzoneJs extends FormElement {
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderDropzoneJs($element) {
// Convert the human size input to bytes, convert it to MB and round it.
$max_size = round(Bytes::toInt($element['#max_filesize']) / pow(Bytes::KILOBYTE, 2), 2);
$element['#attached']['drupalSettings']['dropzonejs'] = [
'upload_path' => \Drupal::url('dropzonejs.upload'),
'instances' => [
// Configuration keys are matched with DropzoneJS configuration
// options.
$element['#id'] => [
'maxFilesize' => $element['#max_filesize'],
'maxFilesize' => $max_size,
'dictDefaultMessage' => $element['#dropzone_description'],
'acceptedFiles' => '.' . str_replace(' ', ',.', self::getValidExtensions()),
'acceptedFiles' => '.' . str_replace(' ', ',.', self::getValidExtensions($element)),
],
],
];
......@@ -110,7 +116,7 @@ class DropzoneJs extends FormElement {
$return['uploaded_files'] = NULL;
if ($input !== FALSE) {
$user_input = NestedArray::getValue($form_state->getUserInput(), $element['#parents'], $key_exists);
$user_input = NestedArray::getValue($form_state->getUserInput(), $element['#parents'] + ['uploaded_files']);
if (!empty($user_input['uploaded_files'])) {
$file_names = array_filter(explode(';', $user_input['uploaded_files']));
......@@ -125,14 +131,17 @@ class DropzoneJs extends FormElement {
// security reasons. Because here we know the acceptable extensions
// we can remove that extension and sanitize the filename.
$name = self::fixTmpFilename($name);
$name = file_munge_filename($name, self::getValidExtensions());
$name = file_munge_filename($name, self::getValidExtensions($element));
// Finaly rename the file and add it to results.
$new_filepath = "$temp_path/$name";
$move_result = file_unmanaged_move($old_filepath, $new_filepath);
if ($move_result) {
$return['uploaded_files'][] = $move_result;
$return['uploaded_files'][] = [
'path' => $move_result,
'filename' => $name,
];
}
else {
drupal_set_message(t('There was a problem while processing the file named @name', ['@name' => $name]), 'error');
......@@ -148,10 +157,13 @@ class DropzoneJs extends FormElement {
/**
* Gets valid file extensions for this element.
*
* @param array $element
* The element array.
*
* @return string
* A space separated list of extensions.
*/
public static function getValidExtensions() {
public static function getValidExtensions($element) {
return isset($element['#extensions']) ? $element['#extensions'] : self::DEFAULT_VALID_EXTENSIONS;
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment