3 namespace Drupal\dropzonejs\Element;
5 use Drupal\Component\Utility\Bytes;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Component\Utility\NestedArray;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\Core\Render\Element\FormElement;
10 use Drupal\Core\StringTranslation\TranslatableMarkup;
14 * Provides a DropzoneJS atop of the file element.
16 * Required options are:
18 * The main field title.
19 * - #description (string)
20 * Description under the field.
21 * - #dropzone_description (string)
22 * Will be visible inside the upload area.
23 * - #max_filesize (string)
24 * Used by dropzonejs and expressed in number + unit (i.e. 1.1M) This will be
25 * converted to a form that DropzoneJs understands. See:
26 * http://www.dropzonejs.com/#config-maxFilesize
27 * - #extensions (string)
28 * A string of valid extensions separated by a space.
29 * - #max_files (integer)
30 * Number of files that can be uploaded.
31 * If < 1, there is no limit.
32 * - #clientside_resize (bool)
33 * Whether or not to use DropzoneJS clientside resizing. It requires v4.4.0+
34 * version of the library.
36 * Optional options are:
37 * - #resize_width (integer)
38 * (optional) The maximum with in px. If omitted defaults to NULL.
39 * - #resize_height (integer)
40 * (optional) The maximum height in px. If omitted defaults to NULL.
41 * - #resize_quality (float)
42 * (optional) The quality of the resize. Accepts values from 0 - 1. Ie: 0.8.
44 * - #resize_method (string)
45 * (optional) Accepts 'contain', which scales the image, or 'crop' which crops
46 * the image. Defaults to 'contain'.
47 * - #thumbnail_method (string).
48 * (optional) Accepts 'contain', which scales the image, or 'crop' which crops
49 * the image. Defaults to 'contain'.
51 * @todo Not sure about the version for clientside.
53 * When submitted the element returns an array of temporary file locations. It's
54 * the duty of the environment that implements this element to handle the
57 * @FormElement("dropzonejs")
59 class DropzoneJs extends FormElement {
62 * A defualut set of valid extensions.
64 const DEFAULT_VALID_EXTENSIONS = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
69 public function getInfo() {
70 $class = get_class($this);
73 '#process' => [[$class, 'processDropzoneJs']],
74 '#pre_render' => [[$class, 'preRenderDropzoneJs']],
75 '#theme' => 'dropzonejs',
76 '#theme_wrappers' => ['form_element'],
79 'library' => ['dropzonejs/integration'],
85 * Processes a dropzone upload element.
87 public static function processDropzoneJs(&$element, FormStateInterface $form_state, &$complete_form) {
88 $element['uploaded_files'] = [
90 // @todo Handle defaults.
91 '#default_value' => '',
92 // If we send a url with a token through drupalSettings the placeholder
93 // doesn't get replaced, because the actual scripts markup is not there
94 // yet. So we pass this information through a data attribute.
95 '#attributes' => ['data-upload-path' => Url::fromRoute('dropzonejs.upload')->toString()],
98 if (empty($element['#max_filesize'])) {
99 $element['#max_filesize'] = file_upload_max_size();
102 // Set #max_files to NULL (explicitly unlimited) if #max_files is not
104 if (empty($element['#max_files'])) {
105 $element['#max_files'] = NULL;
108 if (!\Drupal::currentUser()->hasPermission('dropzone upload files')) {
109 $element['#access'] = FALSE;
110 drupal_set_message(new TranslatableMarkup("You don't have sufficent permissions to use the DropzoneJS uploader. Contact your system administrator"), 'warning');
117 * Prepares a #type 'dropzone' render element for dropzonejs.html.twig.
119 * @param array $element
120 * An associative array containing the properties of the element.
121 * Properties used: #title, #description, #required, #attributes,
122 * #dropzone_description, #max_filesize.
125 * The $element with prepared variables ready for input.html.twig.
127 public static function preRenderDropzoneJs(array $element) {
128 // Convert the human size input to bytes, convert it to MB and round it.
129 $max_size = round(Bytes::toInt($element['#max_filesize']) / pow(Bytes::KILOBYTE, 2), 2);
131 $element['#attached']['drupalSettings']['dropzonejs'] = [
133 // Configuration keys are matched with DropzoneJS configuration
136 'maxFilesize' => $max_size,
137 'dictDefaultMessage' => Html::escape($element['#dropzone_description']),
138 'acceptedFiles' => '.' . str_replace(' ', ',.', self::getValidExtensions($element)),
139 'maxFiles' => $element['#max_files'],
144 if (!empty($element['#clientside_resize'])) {
145 $element['#attached']['drupalSettings']['dropzonejs']['instances'][$element['#id']] += [
146 'resizeWidth' => !empty($element['#resize_width']) ? $element['#resize_width'] : NULL,
147 'resizeHeight' => !empty($element['#resize_height']) ? $element['#resize_height'] : NULL,
148 'resizeQuality' => !empty($element['#resize_quality']) ? $element['#resize_quality'] : 1,
149 'resizeMethod' => !empty($element['#resize_method']) ? $element['#resize_method'] : 'contain',
150 'thumbnailMethod' => !empty($element['#thumbnail_method']) ? $element['#thumbnail_method'] : 'contain',
152 array_unshift($element['#attached']['library'], 'dropzonejs/exif-js');
155 static::setAttributes($element, ['dropzone-enable']);
162 public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
163 $return['uploaded_files'] = [];
165 if ($input !== FALSE) {
166 $user_input = NestedArray::getValue($form_state->getUserInput(), $element['#parents'] + ['uploaded_files']);
168 if (!empty($user_input['uploaded_files'])) {
169 $file_names = array_filter(explode(';', $user_input['uploaded_files']));
170 $tmp_upload_scheme = \Drupal::configFactory()->get('dropzonejs.settings')->get('tmp_upload_scheme');
172 foreach ($file_names as $name) {
173 // The upload handler appended the txt extension to the file for
174 // security reasons. We will remove it in this callback.
175 $old_filepath = $tmp_upload_scheme . '://' . $name;
177 // The upload handler appended the txt extension to the file for
178 // security reasons. Because here we know the acceptable extensions
179 // we can remove that extension and sanitize the filename.
180 $name = self::fixTmpFilename($name);
181 $name = file_munge_filename($name, self::getValidExtensions($element));
183 // Potentially we moved the file already, so let's check first whether
184 // we still have to move.
185 if (file_exists($old_filepath)) {
186 // Finaly rename the file and add it to results.
187 $new_filepath = $tmp_upload_scheme . '://' . $name;
188 $move_result = file_unmanaged_move($old_filepath, $new_filepath);
191 $return['uploaded_files'][] = [
192 'path' => $move_result,
197 drupal_set_message(self::t('There was a problem while processing the file named @name', ['@name' => $name]), 'error');
202 $form_state->setValueForElement($element, $return);
208 * Gets valid file extensions for this element.
210 * @param array $element
214 * A space separated list of extensions.
216 public static function getValidExtensions(array $element) {
217 return isset($element['#extensions']) ? $element['#extensions'] : self::DEFAULT_VALID_EXTENSIONS;
221 * Fix temporary filename.
223 * The upload handler appended the txt extension to the file for
226 * @param string $filename
227 * The filename we need to fix.
230 * The fixed filename.
232 public static function fixTmpFilename($filename) {
233 $parts = explode('.', $filename);
235 return implode('.', $parts);