3 namespace Drupal\dropzonejs_eb_widget\Plugin\EntityBrowser\Widget;
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;
20 * Provides an Entity Browser widget that uploads new files.
22 * @EntityBrowserWidget(
24 * label = @Translation("DropzoneJS"),
25 * description = @Translation("Adds DropzoneJS upload integration."),
29 class DropzoneJsEbWidget extends WidgetBase {
32 * DropzoneJS module upload save service.
34 * @var \Drupal\dropzonejs\DropzoneJsUploadSaveInterface
36 protected $dropzoneJsUploadSave;
39 * Current user service.
41 * @var \Drupal\Core\Session\AccountProxyInterface
43 protected $currentUser;
48 * @var \Drupal\Core\Utility\Token
53 * Constructs widget plugin.
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
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;
84 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $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')
101 public function defaultConfiguration() {
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();
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);
117 $validators = $form_state->get(['entity_browser', 'validators']);
118 if (!empty($validators['cardinality']['cardinality'])) {
119 $cardinality = $validators['cardinality']['cardinality'];
121 $config = $this->getConfiguration();
123 '#title' => $this->t('File upload'),
124 '#type' => 'dropzonejs',
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,
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';
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'] = [
141 '#name' => 'auto_select_handler',
142 '#id' => 'auto_select_handler',
143 '#attributes' => ['id' => 'auto_select_handler'],
144 '#submit' => ['::submitForm'],
145 '#executes_submit_callback' => TRUE,
147 'wrapper' => 'auto_select_handler',
148 'callback' => [get_class($this), 'handleAjaxCommand'],
149 'event' => 'auto_select_enity_browser_widget',
151 'type' => 'fullscreen',
163 public function prepareEntities(array $form, FormStateInterface $form_state) {
164 return $this->getFiles($form, $form_state);
168 * Gets uploaded files.
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.
175 * @param FormStateInterface $form_state
178 * @return \Drupal\file\FileInterface[]
179 * Array of uploaded files.
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]];
185 $files = $form_state->get(['dropzonejs', $this->uuid(), 'files']);
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(
196 $this->getUploadLocation(),
197 $config['settings']['extensions'],
199 $additional_validators
205 if ($form['widget']['upload']['#max_files']) {
206 $files = array_slice($files, -$form['widget']['upload']['#max_files']);
209 $form_state->set(['dropzonejs', $this->uuid(), 'files'], $files);
215 * Gets upload location.
218 * Destination folder URI.
220 protected function getUploadLocation() {
221 return $this->token->replace($this->configuration['upload_location']);
227 public function validate(array &$form, FormStateInterface $form_state) {
228 $trigger = $form_state->getTriggeringElement();
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']]));
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']]));
246 $form_state->setError($form['widget']['upload'], $this->t('At least one valid file should be uploaded.'));
249 // If there weren't any errors set, run the normal validators.
250 if (empty($form_state->getErrors())) {
251 parent::validate($form, $form_state);
259 public function submit(array &$element, array &$form, FormStateInterface $form_state) {
261 foreach ($this->prepareEntities($form, $form_state) as $file) {
262 $file->setPermanent();
267 $this->selectEntities($files, $form_state);
268 $this->clearFormValues($element, $form_state);
274 protected function selectEntities(array $entities, FormStateInterface $form_state) {
275 if (!empty(array_filter($entities))) {
276 $config = $this->getConfiguration();
278 if (empty($config['settings']['auto_select'])) {
279 parent::selectEntities($entities, $form_state);
283 $form_state->set(['dropzonejs', 'added_entities'], $entities);
287 * Clear values from upload form element.
289 * @param array $element
290 * Upload form element.
291 * @param \Drupal\Core\Form\FormStateInterface $form_state
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'], []);
303 * Validate extension.
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.
309 * @param string $filename
310 * The filename we want to test.
311 * @param string $extensions
312 * A space separated list of extensions.
315 * True if the file's extension is a valid one. False otherwise.
317 protected function validateExtension($filename, $extensions) {
318 $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote($extensions)) . ')$/i';
319 if (!preg_match($regex, $filename)) {
328 public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
329 $form = parent::buildConfigurationForm($form, $form_state);
331 $configuration = $this->configuration;
333 $form['upload_location'] = [
334 '#type' => 'textfield',
335 '#title' => $this->t('Upload location'),
336 '#default_value' => $configuration['upload_location'],
339 $form['dropzone_description'] = [
340 '#type' => 'textfield',
341 '#title' => $this->t('Dropzone drag-n-drop zone text'),
342 '#default_value' => $configuration['dropzone_description'],
345 preg_match('%\d+%', $configuration['max_filesize'], $matches);
346 $max_filesize = !empty($matches) ? array_shift($matches) : '1';
348 $form['max_filesize'] = [
350 '#title' => $this->t('Maximum size of files'),
352 '#field_suffix' => $this->t('MB'),
353 '#default_value' => $max_filesize,
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'],
369 public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
370 $values = $form_state->getValues()['table'][$this->uuid()]['form'];
372 if (!empty($values['extensions'])) {
373 $extensions = explode(' ', $values['extensions']);
376 foreach ($extensions as $extension) {
377 if (preg_match('%^\w*$%', $extension) !== 1) {
383 $form_state->setErrorByName('extensions', $this->t('Invalid extension list format.'));
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';
399 public function __sleep() {
400 return array_diff(parent::__sleep(), ['files']);
404 * Handling of automated submit of uploaded files.
408 * @param \Drupal\Core\Form\FormStateInterface $form_state
411 * @return \Drupal\Core\Ajax\AjaxResponse|array
412 * Returns ajax commands that will be executed in front-end.
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) {
420 '#type' => 'status_messages',
424 // Output correct response if everything passed without any error.
425 $ajax = new AjaxResponse();
427 if (($triggering_element = $form_state->getTriggeringElement()) && $triggering_element['#name'] === 'auto_select_handler') {
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();
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.
441 new InvokeCommand('[data-drupal-selector="edit-upload-uploaded-files"]', 'val', [''])
444 // Add Invoke command to select uploaded entities.
446 new InvokeCommand('.entities-list', 'trigger', [