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 * Configuration options are:
18 * The main field title.
20 * Description under the field.
21 * - #dropzone_description
22 * Will be visible inside the upload area.
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
28 * A string of valid extensions separated by a space.
30 * Number of files that can be uploaded.
31 * If < 1, there is no limit.
33 * When submitted the element returns an array of temporary file locations. It's
34 * the duty of the environment that implements this element to handle the
37 * @FormElement("dropzonejs")
39 class DropzoneJs extends FormElement {
42 * A defualut set of valid extensions.
44 const DEFAULT_VALID_EXTENSIONS = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
49 public function getInfo() {
50 $class = get_class($this);
53 '#process' => [[$class, 'processDropzoneJs']],
54 '#pre_render' => [[$class, 'preRenderDropzoneJs']],
55 '#theme' => 'dropzonejs',
56 '#theme_wrappers' => ['form_element'],
59 'library' => ['dropzonejs/integration'],
65 * Processes a dropzone upload element.
67 public static function processDropzoneJs(&$element, FormStateInterface $form_state, &$complete_form) {
68 $element['uploaded_files'] = [
70 // @todo Handle defaults.
71 '#default_value' => '',
72 // If we send a url with a token through drupalSettings the placeholder
73 // doesn't get replaced, because the actual scripts markup is not there
74 // yet. So we pass this information through a data attribute.
75 '#attributes' => ['data-upload-path' => Url::fromRoute('dropzonejs.upload')->toString()],
78 if (empty($element['#max_filesize'])) {
79 $element['#max_filesize'] = file_upload_max_size();
82 // Set #max_files to NULL (explicitly unlimited) if #max_files is not
84 if (empty($element['#max_files'])) {
85 $element['#max_files'] = NULL;
88 if (!\Drupal::currentUser()->hasPermission('dropzone upload files')) {
89 $element['#access'] = FALSE;
90 drupal_set_message(new TranslatableMarkup("You don't have sufficent permissions to use the DropzoneJS uploader. Contact your system administrator"), 'warning');
97 * Prepares a #type 'dropzone' render element for dropzonejs.html.twig.
99 * @param array $element
100 * An associative array containing the properties of the element.
101 * Properties used: #title, #description, #required, #attributes,
102 * #dropzone_description, #max_filesize.
105 * The $element with prepared variables ready for input.html.twig.
107 public static function preRenderDropzoneJs(array $element) {
108 // Convert the human size input to bytes, convert it to MB and round it.
109 $max_size = round(Bytes::toInt($element['#max_filesize']) / pow(Bytes::KILOBYTE, 2), 2);
111 $element['#attached']['drupalSettings']['dropzonejs'] = [
113 // Configuration keys are matched with DropzoneJS configuration
116 'maxFilesize' => $max_size,
117 'dictDefaultMessage' => Html::escape($element['#dropzone_description']),
118 'acceptedFiles' => '.' . str_replace(' ', ',.', self::getValidExtensions($element)),
119 'maxFiles' => $element['#max_files'],
124 static::setAttributes($element, ['dropzone-enable']);
131 public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
132 $return['uploaded_files'] = [];
134 if ($input !== FALSE) {
135 $user_input = NestedArray::getValue($form_state->getUserInput(), $element['#parents'] + ['uploaded_files']);
137 if (!empty($user_input['uploaded_files'])) {
138 $file_names = array_filter(explode(';', $user_input['uploaded_files']));
139 $tmp_upload_scheme = \Drupal::configFactory()->get('dropzonejs.settings')->get('tmp_upload_scheme');
141 foreach ($file_names as $name) {
142 // The upload handler appended the txt extension to the file for
143 // security reasons. We will remove it in this callback.
144 $old_filepath = $tmp_upload_scheme . '://' . $name;
146 // The upload handler appended the txt extension to the file for
147 // security reasons. Because here we know the acceptable extensions
148 // we can remove that extension and sanitize the filename.
149 $name = self::fixTmpFilename($name);
150 $name = file_munge_filename($name, self::getValidExtensions($element));
152 // Potentially we moved the file already, so let's check first whether
153 // we still have to move.
154 if (file_exists($old_filepath)) {
155 // Finaly rename the file and add it to results.
156 $new_filepath = $tmp_upload_scheme . '://' . $name;
157 $move_result = file_unmanaged_move($old_filepath, $new_filepath);
160 $return['uploaded_files'][] = [
161 'path' => $move_result,
166 drupal_set_message(self::t('There was a problem while processing the file named @name', ['@name' => $name]), 'error');
171 $form_state->setValueForElement($element, $return);
177 * Gets valid file extensions for this element.
179 * @param array $element
183 * A space separated list of extensions.
185 public static function getValidExtensions(array $element) {
186 return isset($element['#extensions']) ? $element['#extensions'] : self::DEFAULT_VALID_EXTENSIONS;
190 * Fix temporary filename.
192 * The upload handler appended the txt extension to the file for
195 * @param string $filename
196 * The filename we need to fix.
199 * The fixed filename.
201 public static function fixTmpFilename($filename) {
202 $parts = explode('.', $filename);
204 return implode('.', $parts);