Commit 3bac3715 authored by slashrsm's avatar slashrsm
Browse files

Merge pull request #1 from primsi/initial_implementation

Initial implementation
parents 8b0cadda 1440a026
......@@ -2,6 +2,19 @@
This is the Drupal 8 integration for [DropzoneJS](http://www.dropzonejs.com/).
###How to install:
1. Download this module
2. [Download DropzoneJS](https://github.com/enyo/dropzone) and place it in the libraries folder
3. Install dropzonejs the [usual way](https://www.drupal.org/documentation/install/modules-themes/modules-8)
You will now have a dropzonejs element at your disposal.
###Future plans:
- A dropzonejs field widget.
- Handling already uploaded files.
- Removing files that failed dropzonejs validation from temp storage.
- Removing files that were removed by the user on first upload from temp storage.
###Project page:
Currently still without a drupal.org repository (https://
www.drupal.org/project/dropzonejs seems like a project without maintainer).
......
name: dropzonejs
type: module
description: DropzoneJS
core: 8.x
package: Media
dependencies:
- file
dropzonejs:
title: 'Dropzonejs'
website: http://www.dropzonejs.com
version: 4.0.1
license:
name: MIT
url: https://github.com/enyo/dropzone/blob/master/LICENSE
gpl-compatible: true
js:
/libraries/dropzone/dist/min/dropzone.min.js: {}
css:
component:
/libraries/dropzone/dist/min/dropzone.min.css: {}
integration:
version: VERSION
js:
js/dropzone.integration.js: {}
<?php
/**
* @file
* Contains dropzonejs.module.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function dropzonejs_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the dropzonejs module.
case 'help.page.dropzonejs':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('DropzoneJS') . '</p>';
return $output;
default:
}
}
/**
* Implements hook_theme().
*/
function dropzonejs_theme() {
return [
'dropzonejs' => [
'render element' => 'element',
],
];
}
/**
* Prepares variables for dropzone form element.
*
* Default template: dropzonejs.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: A render element representing the file.
*/
function template_preprocess_dropzonejs(&$variables) {
$element = $variables['element'];
$variables['attributes'] = [];
if (isset($element['#id'])) {
$variables['attributes']['id'] = $element['#id'];
}
if (!empty($element['#attributes']['class'])) {
$variables['attributes']['class'] = (array) $element['#attributes']['class'];
}
$variables['uploaded_files'] = $element['uploaded_files'];
}
dropzone upload files:
title: 'Dropzone upload files'
description: 'Allow uploading of files via dropzonejs.'
dropzonejs.upload:
path: '/dropzonejs/upload'
defaults:
_controller: '\Drupal\dropzonejs\Controller\UploadController::handleUploads'
requirements:
_permission: 'dropzone upload files'
/**
* @file dropzone.integration.js
*
* Defines the behaviors needed for dropzonejs integration.
*
* @todo Implement maxfilesexceeded.
*
*/
(function ($, Drupal, drupalSettings) {
"use strict";
Drupal.behaviors.dropzonejsIntegraion = {
attach: function(context) {
Dropzone.autoDiscover = false;
var selector = $(".dropzone-enable");
selector.addClass("dropzone");
// Initiate dropzonejs.
var config = {
url: drupalSettings.dropzonejs.upload_path,
addRemoveLinks: true,
};
var instanceConfig = drupalSettings.dropzonejs.instances[selector.attr('id')];
var dropzoneInstance = new Dropzone("#" + selector.attr("id"), $.extend({}, instanceConfig, config));
// React on add file. Add only accepted files.
dropzoneInstance.on("addedfile", function(file) {
var uploadedFilesElement = selector.siblings(':hidden');
var currentValue = uploadedFilesElement.attr('value');
uploadedFilesElement.attr('value', currentValue + file.name + ';');
});
// React on file removing.
dropzoneInstance.on("removedfile", function(file) {
var uploadedFilesElement = selector.siblings(':hidden');
var currentValue = uploadedFilesElement.attr('value');
// Remove the file from the element.
if (currentValue.length) {
var fileNames = currentValue.split(";");
for (var i in fileNames) {
if (fileNames[i] == file.name) {
fileNames.splice(i,1);
break;
}
}
var newValue = fileNames.join(';');
uploadedFilesElement.attr('value', newValue);
}
});
}
};
}(jQuery, Drupal, drupalSettings));
<?php
/**
* @file
* Contains \Drupal\dropzonejs\Controller\UploadController.
*/
namespace Drupal\dropzonejs\Controller;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\dropzonejs\UploadException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Handles requests that dropzone issues when uploading files.
*/
class UploadController extends ControllerBase {
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
*/
protected $request;
/**
* Stores temporary folder URI.
*
* This is configurable via the configuration variable. It was added for HA
* environments where temporary location may need to be a shared across all
* servers.
*
* @var string
*/
protected $temporaryUploadLocation;
/**
* Filename of a file that is being uploaded.
*
* @var string
*/
protected $filename;
/**
* Constructs dropzone upload controller route controller.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Request object.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* Config factory.
*/
public function __construct(Request $request, ConfigFactoryInterface $config) {
$this->request = $request;
$tmp_override = $config->get('dropzonejs.settings')->get('tmp_dir');
$this->temporaryUploadLocation = ($tmp_override) ? $tmp_override : $config->get('system.file')->get('path.temporary');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('request_stack')->getCurrentRequest(),
$container->get('config.factory')
);
}
/**
* Handles DropzoneJs uploads.
*/
public function handleUploads() {
// @todo: Implement file_validate_size();
try {
$this->prepareTemporaryUploadDestination();
$this->handleUpload();
}
catch (UploadException $e) {
return $e->getErrorResponse();
}
}
/**
* Prepares temporary destination folder for uploaded files.
*
* @return bool
* TRUE if destination folder looks OK and FALSE otherwise.
*
* @throws \Drupal\dropzonejs\UploadException
*/
protected function prepareTemporaryUploadDestination() {
$writable = file_prepare_directory($this->temporaryUploadLocation, FILE_CREATE_DIRECTORY);
if (!$writable) {
throw new UploadException(UploadException::DESTINATION_FOLDER_ERROR);
}
// Try to make sure this is private via htaccess.
file_save_htaccess($this->temporaryUploadLocation, TRUE);
}
/**
* Reads, checks and return filename of a file being uploaded.
*
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
* An instance of UploadedFile.
*
* @throws \Drupal\dropzonejs\UploadException
*/
protected function getFilename(UploadedFile $file) {
if (empty($this->filename)) {
$this->filename = $file->getClientOriginalName();
// Check the file name for security reasons; it must contain letters,
// numbers and underscores.
if (!preg_match('/[\w\.]/', $this->filename)) {
throw new UploadException(UploadException::FILENAME_ERROR);
}
}
return $this->filename;
}
/**
* Handles multipart uploads.
*
* @throws \Drupal\dropzonejs\UploadException
* @throws Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
protected function handleUpload() {
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file */
$file = $this->request->files->get('file');
if (!$file instanceof UploadedFile) {
throw new AccessDeniedHttpException();
}
elseif ($file->getError() != UPLOAD_ERR_OK) {
throw new UploadException(UploadException::FILE_UPLOAD_ERROR);
}
// Open temp file.
$tmp = $this->temporaryUploadLocation . $this->getFilename($file);
if (!($out = fopen("{$this->temporaryUploadLocation}/{$this->getFilename($file)}", $this->request->request->get('chunk', 0) ? 'ab' : 'wb'))) {
throw new UploadException(UploadException::OUTPUT_ERROR);
}
// Read binary input stream.
$input_uri = "{$this->temporaryUploadLocation}/{$file->getFilename()}";
if (!($in = fopen($input_uri, 'rb'))) {
throw new UploadException(UploadException::INPUT_ERROR);
}
// Append input stream to temp file.
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
// Be nice and keep everything nice and clean.
// @todo when implementing multipart dont forget to drupal_unlink.
fclose($in);
fclose($out);
}
}
<?php
/**
* @file
* Contains \Drupal\dropzonejs\src\Element.
*/
namespace Drupal\dropzonejs\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
/**
* Provides a DropzoneJS atop of the file element.
*
* Configuration options are:
* - #title
* The main field title.
* - #description
* Description under the field.
* - #dropzone_description
* Will be visible inside the upload area.
* - #max_filesize
* Used by dropzonejs and expressed in MB. See
* http://www.dropzonejs.com/#config-maxFilesize
*
*
* @todo Remove updated_files from the values array.
*
* @FormElement("dropzonejs")
*/
class DropzoneJs extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#multiple' => FALSE,
'#process' => [[$class, 'processDropzoneJs']],
'#size' => 60,
'#pre_render' => [[$class, 'preRenderDropzoneJs']],
'#theme' => 'dropzonejs',
'#theme_wrappers' => ['form_element'],
'#attached' => [
'library' => ['dropzonejs/dropzonejs', 'dropzonejs/integration']
],
];
}
/**
* Processes a dropzone upload element, make use of #multiple if present.
*/
public static function processDropzoneJs(&$element, FormStateInterface $form_state, &$complete_form) {
$element['uploaded_files'] = [
'#type' => 'hidden',
// @todo Handle defaults.
'#default_value' => '',
];
return $element;
}
/**
* Prepares a #type 'dropzone' render element for dropzonejs.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #description, #required, #attributes,
* #dropzone_description, #max_filesize.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderDropzoneJs($element) {
$element['#attached']['drupalSettings']['dropzonejs'] = [
'upload_path' => base_path() . 'dropzonejs/upload',
'instances' => [
$element['#id'] => [
'maxFilesize' => $element['#max_filesize'],
'dictDefaultMessage' => $element['#dropzone_description']
],
],
];
static::setAttributes($element, ['dropzone-enable']);
return $element;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
$file_names = [];
$return = NULL;
if ($input !== FALSE) {
$user_input = $form_state->getUserInput();
if (!empty($user_input['uploaded_files'])) {
$file_names = array_filter(explode(';', $user_input['uploaded_files']));
$temp_path = \Drupal::config('system.file')->get('path.temporary');
foreach ($file_names as $name) {
$return[] = "$temp_path/$name";
}
}
$form_state->setValueForElement($element, $return);
return $return;
}
}
}
<?php
/**
* @file
* Contains \Drupal\dropzonejs\DropzoneJsUploadException.
*/
namespace Drupal\dropzonejs;
use Symfony\Component\HttpFoundation\JsonResponse;
class UploadExceptionn extends \Exception {
/**
* Error with input stream.
*/
const INPUT_ERROR = 101;
/**
* Error with output stream.
*/
const OUTPUT_ERROR = 102;
/**
* Error moving uploaded file.
*/
const MOVE_ERROR = 103;
/**
* Error with destination folder.
*/
const DESTINATION_FOLDER_ERROR = 104;
/**
* Error with temporary file name.
*/
const FILENAME_ERROR = 105;
/**
* File upload resulted in error.
*/
const FILE_UPLOAD_ERROR = 106;
/**
* Code to error message mapping.
*
* @param array $code
*/
public $errorMessages = array(
self::INPUT_ERROR => 'Failed to open input stream.',
self::OUTPUT_ERROR => 'Failed to open output stream.',
self::MOVE_ERROR => 'Failed to move uploaded file.',
self::DESTINATION_FOLDER_ERROR => 'Failed to open temporary directory.',
self::FILENAME_ERROR => 'Invalid temporary file name.',
self::FILE_UPLOAD_ERROR => 'The file upload resulted in an error on php level. See http://php.net/manual/en/features.file-upload.errors.php',
);
/**
* Constructs UploadException.
*
* @param int $code
* Error code.
*/
public function __construct($code) {
$this->code = $code;
$this->message = $this->errorMessages[$this->code];
}
/**
* Generates and returns JSON response object for the error.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* JSON response object.
*/
public function getErrorResponse() {
return new JsonResponse(
array(
'jsonrpc' => '2.0',
'error' => array(
'code' => $this->code,
'message' => $this->errorMessages[$this->code],
),
'id' => 'id',
),
500
);
}
}
{#
/**
* @file
* Theme implementation for the dropzonejs form element.
*
* Available variables:
* - attributes: A list of HTML attributes for the element.
* - children: Optional additional rendered elements.
* - uploaded_files: Hidden element that holds uploaded files.
*
* @see template_preprocess_dropzonejs()
*/
#}
<div{{ attributes }} /></div>
{{ uploaded_files }}
{{ children }}
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