Version 1
[yaffs-website] / web / modules / contrib / dropzonejs / modules / eb_widget / src / Plugin / EntityBrowser / Widget / DropzoneJsEbWidget.php
1 <?php
2
3 namespace Drupal\dropzonejs_eb_widget\Plugin\EntityBrowser\Widget;
4
5 use Drupal\Component\Utility\Bytes;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Core\Ajax\AjaxResponse;
8 use Drupal\Core\Ajax\InvokeCommand;
9 use Drupal\Core\Entity\EntityTypeManagerInterface;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Session\AccountProxyInterface;
12 use Drupal\Core\Utility\Token;
13 use Drupal\dropzonejs\DropzoneJsUploadSaveInterface;
14 use Drupal\entity_browser\WidgetBase;
15 use Drupal\entity_browser\WidgetValidationManager;
16 use Symfony\Component\DependencyInjection\ContainerInterface;
17 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18
19 /**
20  * Provides an Entity Browser widget that uploads new files.
21  *
22  * @EntityBrowserWidget(
23  *   id = "dropzonejs",
24  *   label = @Translation("DropzoneJS"),
25  *   description = @Translation("Adds DropzoneJS upload integration."),
26  *   auto_select = TRUE
27  * )
28  */
29 class DropzoneJsEbWidget extends WidgetBase {
30
31   /**
32    * DropzoneJS module upload save service.
33    *
34    * @var \Drupal\dropzonejs\DropzoneJsUploadSaveInterface
35    */
36   protected $dropzoneJsUploadSave;
37
38   /**
39    * Current user service.
40    *
41    * @var \Drupal\Core\Session\AccountProxyInterface
42    */
43   protected $currentUser;
44
45   /**
46    * The token service.
47    *
48    * @var \Drupal\Core\Utility\Token
49    */
50   protected $token;
51
52   /**
53    * Constructs widget plugin.
54    *
55    * @param array $configuration
56    *   A configuration array containing information about the plugin instance.
57    * @param string $plugin_id
58    *   The plugin_id for the plugin instance.
59    * @param mixed $plugin_definition
60    *   The plugin implementation definition.
61    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
62    *   Event dispatcher service.
63    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
64    *   The entity type manager service.
65    * @param \Drupal\entity_browser\WidgetValidationManager $validation_manager
66    *   The Widget Validation Manager service.
67    * @param \Drupal\dropzonejs\DropzoneJsUploadSaveInterface $dropzonejs_upload_save
68    *   The upload saving dropzonejs service.
69    * @param \Drupal\Core\Session\AccountProxyInterface $current_user
70    *   The current user service.
71    * @param \Drupal\Core\Utility\Token $token
72    *   The token service.
73    */
74   public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, WidgetValidationManager $validation_manager, DropzoneJsUploadSaveInterface $dropzonejs_upload_save, AccountProxyInterface $current_user, Token $token) {
75     parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_type_manager, $validation_manager);
76     $this->dropzoneJsUploadSave = $dropzonejs_upload_save;
77     $this->currentUser = $current_user;
78     $this->token = $token;
79   }
80
81   /**
82    * {@inheritdoc}
83    */
84   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
85     return new static(
86       $configuration,
87       $plugin_id,
88       $plugin_definition,
89       $container->get('event_dispatcher'),
90       $container->get('entity_type.manager'),
91       $container->get('plugin.manager.entity_browser.widget_validation'),
92       $container->get('dropzonejs.upload_save'),
93       $container->get('current_user'),
94       $container->get('token')
95     );
96   }
97
98   /**
99    * {@inheritdoc}
100    */
101   public function defaultConfiguration() {
102     return [
103       'upload_location' => 'public://[date:custom:Y]-[date:custom:m]',
104       'dropzone_description' => $this->t('Drop files here to upload them'),
105       'max_filesize' => file_upload_max_size() / pow(Bytes::KILOBYTE, 2) . 'M',
106       'extensions' => 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp',
107     ] + parent::defaultConfiguration();
108   }
109
110   /**
111    * {@inheritdoc}
112    */
113   public function getForm(array &$original_form, FormStateInterface $form_state, array $additional_widget_parameters) {
114     $form = parent::getForm($original_form, $form_state, $additional_widget_parameters);
115
116     $cardinality = 0;
117     $validators = $form_state->get(['entity_browser', 'validators']);
118     if (!empty($validators['cardinality']['cardinality'])) {
119       $cardinality = $validators['cardinality']['cardinality'];
120     }
121     $config = $this->getConfiguration();
122     $form['upload'] = [
123       '#title' => $this->t('File upload'),
124       '#type' => 'dropzonejs',
125       '#required' => TRUE,
126       '#dropzone_description' => $config['settings']['dropzone_description'],
127       '#max_filesize' => $config['settings']['max_filesize'],
128       '#extensions' => $config['settings']['extensions'],
129       '#max_files' => ($cardinality > 0) ? $cardinality : 0,
130     ];
131
132     $form['#attached']['library'][] = 'dropzonejs/widget';
133     // Disable the submit button until the upload sucesfully completed.
134     $form['#attached']['library'][] = 'dropzonejs_eb_widget/common';
135     $original_form['#attributes']['class'][] = 'dropzonejs-disable-submit';
136
137     // Add hidden element used to make execution of auto-select of form.
138     if (!empty($config['settings']['auto_select'])) {
139       $form['auto_select_handler'] = [
140         '#type' => 'hidden',
141         '#name' => 'auto_select_handler',
142         '#id' => 'auto_select_handler',
143         '#attributes' => ['id' => 'auto_select_handler'],
144         '#submit' => ['::submitForm'],
145         '#executes_submit_callback' => TRUE,
146         '#ajax' => [
147           'wrapper' => 'auto_select_handler',
148           'callback' => [get_class($this), 'handleAjaxCommand'],
149           'event' => 'auto_select_enity_browser_widget',
150           'progress' => [
151             'type' => 'fullscreen',
152           ],
153         ],
154       ];
155     }
156
157     return $form;
158   }
159
160   /**
161    * {@inheritdoc}
162    */
163   public function prepareEntities(array $form, FormStateInterface $form_state) {
164     return $this->getFiles($form, $form_state);
165   }
166
167   /**
168    * Gets uploaded files.
169    *
170    * We implement this to allow child classes to operate on different entity
171    * type while still having access to the files in the validate callback here.
172    *
173    * @param array $form
174    *   Form structure.
175    * @param FormStateInterface $form_state
176    *   Form state object.
177    *
178    * @return \Drupal\file\FileInterface[]
179    *   Array of uploaded files.
180    */
181   protected function getFiles(array $form, FormStateInterface $form_state) {
182     $config = $this->getConfiguration();
183     $additional_validators = ['file_validate_size' => [Bytes::toInt($config['settings']['max_filesize']), 0]];
184
185     $files = $form_state->get(['dropzonejs', $this->uuid(), 'files']);
186
187     if (!$files) {
188       $files = [];
189     }
190
191     // We do some casting because $form_state->getValue() might return NULL.
192     foreach ((array) $form_state->getValue(['upload', 'uploaded_files'], []) as $file) {
193       if (file_exists($file['path'])) {
194         $entity = $this->dropzoneJsUploadSave->createFile(
195           $file['path'],
196           $this->getUploadLocation(),
197           $config['settings']['extensions'],
198           $this->currentUser,
199           $additional_validators
200         );
201         $files[] = $entity;
202       }
203     }
204
205     if ($form['widget']['upload']['#max_files']) {
206       $files = array_slice($files, -$form['widget']['upload']['#max_files']);
207     }
208
209     $form_state->set(['dropzonejs', $this->uuid(), 'files'], $files);
210
211     return $files;
212   }
213
214   /**
215    * Gets upload location.
216    *
217    * @return string
218    *   Destination folder URI.
219    */
220   protected function getUploadLocation() {
221     return $this->token->replace($this->configuration['upload_location']);
222   }
223
224   /**
225    * {@inheritdoc}
226    */
227   public function validate(array &$form, FormStateInterface $form_state) {
228     $trigger = $form_state->getTriggeringElement();
229
230     // Validate if we are in fact uploading a files and all of them have the
231     // right extensions. Although DropzoneJS should not even upload those files
232     // it's still better not to rely only on client side validation.
233     if (($trigger['#type'] == 'submit' && $trigger['#name'] == 'op') || $trigger['#name'] === 'auto_select_handler') {
234       $upload_location = $this->getUploadLocation();
235       if (!file_prepare_directory($upload_location, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
236         $form_state->setError($form['widget']['upload'], $this->t('Files could not be uploaded because the destination directory %destination is not configured correctly.', ['%destination' => $this->getConfiguration()['settings']['upload_location']]));
237       }
238
239       $files = $this->getFiles($form, $form_state);
240       if (in_array(FALSE, $files)) {
241         // @todo Output the actual errors from validateFile.
242         $form_state->setError($form['widget']['upload'], $this->t('Some files that you are trying to upload did not pass validation. Requirements are: max file %size, allowed extensions are %extensions', ['%size' => $this->getConfiguration()['settings']['max_filesize'], '%extensions' => $this->getConfiguration()['settings']['extensions']]));
243       }
244
245       if (empty($files)) {
246         $form_state->setError($form['widget']['upload'], $this->t('At least one valid file should be uploaded.'));
247       }
248
249       // If there weren't any errors set, run the normal validators.
250       if (empty($form_state->getErrors())) {
251         parent::validate($form, $form_state);
252       }
253     }
254   }
255
256   /**
257    * {@inheritdoc}
258    */
259   public function submit(array &$element, array &$form, FormStateInterface $form_state) {
260     $files = [];
261     foreach ($this->prepareEntities($form, $form_state) as $file) {
262       $file->setPermanent();
263       $file->save();
264       $files[] = $file;
265     }
266
267     $this->selectEntities($files, $form_state);
268     $this->clearFormValues($element, $form_state);
269   }
270
271   /**
272    * {@inheritdoc}
273    */
274   protected function selectEntities(array $entities, FormStateInterface $form_state) {
275     if (!empty(array_filter($entities))) {
276       $config = $this->getConfiguration();
277
278       if (empty($config['settings']['auto_select'])) {
279         parent::selectEntities($entities, $form_state);
280       }
281     }
282
283     $form_state->set(['dropzonejs', 'added_entities'], $entities);
284   }
285
286   /**
287    * Clear values from upload form element.
288    *
289    * @param array $element
290    *   Upload form element.
291    * @param \Drupal\Core\Form\FormStateInterface $form_state
292    *   Form state object.
293    */
294   protected function clearFormValues(array &$element, FormStateInterface $form_state) {
295     // We propagated entities to the other parts of the system. We can now
296     // remove them from our values.
297     $form_state->setValueForElement($element['upload']['uploaded_files'], '');
298     NestedArray::setValue($form_state->getUserInput(), $element['upload']['uploaded_files']['#parents'], '');
299     $form_state->set(['dropzonejs', $this->uuid(), 'files'], []);
300   }
301
302   /**
303    * Validate extension.
304    *
305    * Because while validating we don't have a file object yet, we can't use
306    * file_validate_extensions directly. That's why we make a copy of that
307    * function here and switch the file argument with filename argument.
308    *
309    * @param string $filename
310    *   The filename we want to test.
311    * @param string $extensions
312    *   A space separated list of extensions.
313    *
314    * @return bool
315    *   True if the file's extension is a valid one. False otherwise.
316    */
317   protected function validateExtension($filename, $extensions) {
318     $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
319     if (!preg_match($regex, $filename)) {
320       return FALSE;
321     }
322     return TRUE;
323   }
324
325   /**
326    * {@inheritdoc}
327    */
328   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
329     $form = parent::buildConfigurationForm($form, $form_state);
330
331     $configuration = $this->configuration;
332
333     $form['upload_location'] = [
334       '#type' => 'textfield',
335       '#title' => $this->t('Upload location'),
336       '#default_value' => $configuration['upload_location'],
337     ];
338
339     $form['dropzone_description'] = [
340       '#type' => 'textfield',
341       '#title' => $this->t('Dropzone drag-n-drop zone text'),
342       '#default_value' => $configuration['dropzone_description'],
343     ];
344
345     preg_match('%\d+%', $configuration['max_filesize'], $matches);
346     $max_filesize = !empty($matches) ? array_shift($matches) : '1';
347
348     $form['max_filesize'] = [
349       '#type' => 'number',
350       '#title' => $this->t('Maximum size of files'),
351       '#min' => '0',
352       '#field_suffix' => $this->t('MB'),
353       '#default_value' => $max_filesize,
354     ];
355
356     $form['extensions'] = [
357       '#type' => 'textfield',
358       '#title' => $this->t('Allowed file extensions'),
359       '#desciption' => $this->t('A space separated list of file extensions'),
360       '#default_value' => $configuration['extensions'],
361     ];
362
363     return $form;
364   }
365
366   /**
367    * {@inheritdoc}
368    */
369   public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
370     $values = $form_state->getValues()['table'][$this->uuid()]['form'];
371
372     if (!empty($values['extensions'])) {
373       $extensions = explode(' ', $values['extensions']);
374       $fail = FALSE;
375
376       foreach ($extensions as $extension) {
377         if (preg_match('%^\w*$%', $extension) !== 1) {
378           $fail = TRUE;
379         }
380       }
381
382       if ($fail) {
383         $form_state->setErrorByName('extensions', $this->t('Invalid extension list format.'));
384       }
385     }
386   }
387
388   /**
389    * {@inheritdoc}
390    */
391   public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
392     parent::submitConfigurationForm($form, $form_state);
393     $this->configuration['max_filesize'] = $this->configuration['max_filesize'] . 'M';
394   }
395
396   /**
397    * {@inheritdoc}
398    */
399   public function __sleep() {
400     return array_diff(parent::__sleep(), ['files']);
401   }
402
403   /**
404    * Handling of automated submit of uploaded files.
405    *
406    * @param array $form
407    *   Form.
408    * @param \Drupal\Core\Form\FormStateInterface $form_state
409    *   Form state.
410    *
411    * @return \Drupal\Core\Ajax\AjaxResponse|array
412    *   Returns ajax commands that will be executed in front-end.
413    */
414   public static function handleAjaxCommand(array $form, FormStateInterface $form_state) {
415     // If there are some errors during submitting of form they should be
416     // displayed, that's why we are returning status message here and generated
417     // errors will be displayed properly in front-end.
418     if (count($form_state->getErrors()) > 0) {
419       return [
420         '#type' => 'status_messages',
421       ];
422     }
423
424     // Output correct response if everything passed without any error.
425     $ajax = new AjaxResponse();
426
427     if (($triggering_element = $form_state->getTriggeringElement()) && $triggering_element['#name'] === 'auto_select_handler') {
428       $entity_ids = [];
429
430       $added_entities = $form_state->get(['dropzonejs', 'added_entities']);
431       /** @var \Drupal\Core\Entity\EntityInterface $entity */
432       if (!empty($added_entities)) {
433         foreach ($added_entities as $entity) {
434           $entity_ids[] = $entity->getEntityTypeId() . ':' . $entity->id();
435         }
436       }
437
438       // Add command to clear list of uploaded files. It's important to set
439       // empty string value, in other case it will act as getter.
440       $ajax->addCommand(
441         new InvokeCommand('[data-drupal-selector="edit-upload-uploaded-files"]', 'val', [''])
442       );
443
444       // Add Invoke command to select uploaded entities.
445       $ajax->addCommand(
446         new InvokeCommand('.entities-list', 'trigger', [
447           'add-entities',
448           [$entity_ids],
449         ])
450       );
451     }
452
453     return $ajax;
454   }
455
456 }