webdam-1.0.x-dev/src/Plugin/EntityBrowser/Widget/WebdamUpload.php
src/Plugin/EntityBrowser/Widget/WebdamUpload.php
<?php
namespace Drupal\webdam\Plugin\EntityBrowser\Widget;
use Drupal\Core\Utility\Error;
use Drupal\webdam\WebdamConnector;
use Drupal\webdam\Exception\BundleNotWebdamException;
use Drupal\webdam\Exception\BundleNotExistException;
use Drupal\webdam\Exception\WebdamException;
use Drupal\webdam\Exception\UploadFailedException;
use Drupal\webdam\Plugin\Field\FieldType\WebdamMetadataItem;
use Drupal\webdam\Plugin\media\Source\Webdam;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Url;
use Drupal\entity_browser\WidgetValidationManager;
use Drupal\media\Entity\Media;
use Drupal\media\MediaInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Drupal\Core\Entity\EntityStorageException;
/**
* Uses upload to create media entities.
*
* @EntityBrowserWidget(
* id = "webdam_upload",
* label = @Translation("Webdam upload"),
* description = @Translation("Uploads files to Webdam and creates wrapping media entities."),
* provider = "dropzonejs",
* )
*/
class WebdamUpload extends WebdamWidgetBase {
/**
* Number of times to try fetching an asset during the batch.
*/
const FAIL_LIMIT = 30;
/**
* The session service.
*
* @var \Symfony\Component\HttpFoundation\Session\SessionInterface
*/
protected $session;
/**
* Upload constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Event dispatcher service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\entity_browser\WidgetValidationManager $validation_manager
* The Widget Validation Manager service.
* @param \Drupal\webdam\WebdamConnector $webdam_connector
* Webdam connector.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* Config factory.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* Logger factory.
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* The session service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, WidgetValidationManager $validation_manager, WebdamConnector $webdam_connector, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, SessionInterface $session, LanguageManagerInterface $language_manager, RequestStack $request_stack) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_type_manager, $validation_manager, $webdam_connector, $logger_factory, $language_manager, $request_stack, $config_factory);
$this->session = $session;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('event_dispatcher'),
$container->get('entity_type.manager'),
$container->get('plugin.manager.entity_browser.widget_validation'),
$container->get('webdam.connector'),
$container->get('config.factory'),
$container->get('logger.factory'),
$container->get('session'),
$container->get('language_manager'),
$container->get('request_stack')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'extensions' => 'jpg jpeg png gif',
'dropzone_description' => $this->t('Drop files here to upload them.'),
'tags' => [],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function getForm(array &$original_form, FormStateInterface $form_state, array $additional_widget_parameters) {
$form = parent::getForm($original_form, $form_state, $additional_widget_parameters);
if ($form_state->getValue('errors')) {
$form['actions']['submit']['#access'] = FALSE;
return $form;
}
if ($form_state->getValue('errors')) {
$form['actions']['submit']['#access'] = FALSE;
return $form;
}
$form['upload'] = [
'#title' => $this->t('File upload'),
'#type' => 'dropzonejs',
'#dropzone_description' => $this->getConfiguration()['settings']['dropzone_description'],
];
$folder_options = [];
// @todo Deprecation notice: Return type of Bynder\webdam\Entity\Folder::jsonSerialize() should either be compatible with JsonSerializable::jsonSerialize():
// We suppress the errors for the moment.
try {
$client = $this->webdamConnector->getClient();
$top_folders = @$client->getTopLevelFolders();
// @todo Cache this? Ask if we cache.
$this->buildFolderOptions($top_folders, $folder_options);
}
catch (\Exception $exception) {
$this->messenger()->addError($exception->getMessage());
}
$form['folder'] = [
'#type' => 'select',
'#options' => $folder_options,
'#title' => $this->t('Folder'),
'#required' => TRUE,
];
if ($uploaded_assets = $this->session->get('webdam_upload_batch_result', [])) {
$form_state->set('uploaded_entities', $uploaded_assets);
$this->session->remove('webdam_upload_batch_result');
$form['upload']['#access'] = FALSE;
$form['#attached']['library'][] = 'webdam/upload';
$form['actions']['submit']['#attributes']['class'][] = 'visually-hidden';
$form['message']['#markup'] = $this->t('Finishing upload. Please wait...');
}
else {
$form['actions']['submit']['#eb_widget_main_submit'] = FALSE;
$form['actions']['submit']['#webdam_upload_submit'] = TRUE;
}
return $form;
}
/**
* Build folder options array.
*
* @param \Bynder\webdam\Entity\Folder[] $folders
* An array containing folders.
* @param array $folder_options
* The options, that we are building, are passed by reference.
* @param int $level
* The nesting level, we are currently in.
*/
protected function buildFolderOptions(array $folders, array &$folder_options, int $level = -1) {
$level++;
foreach ($folders as $folder) {
// @todo There seem to be permissions, but I am not sure how do we set
// them. Ask them.
$option = str_repeat('-', $level) . " $folder->name";
if (!in_array('upload', $folder->permissions->assets)) {
$option .= ' (' . $this->t('protected') . ')';
}
$folder_options[$folder->id] = $option;
if ((int) $folder->numchildren > 0) {
$loaded_folder = $this->webdamConnector->getClient()
->getFolder($folder->id);
$this->buildFolderOptions($loaded_folder->folders, $folder_options, $level);
}
}
}
/**
* {@inheritdoc}
*/
protected function prepareEntities(array $form, FormStateInterface $form_state) {
if ($entities = $form_state->get('uploaded_entities')) {
return $entities;
}
return [];
}
/**
* {@inheritdoc}
*/
public function submit(array &$element, array &$form, FormStateInterface $form_state) {
if (!empty($form_state->getTriggeringElement()['#webdam_upload_submit'])) {
/** @var \Drupal\media\MediaTypeInterface $type */
$type = $this->entityTypeManager->getStorage('media_type')
->load($this->configuration['media_type']);
if ($type && ($type->getSource()) instanceof Webdam) {
$form_state->setRebuild();
$batch = [
'title' => $this->t('Uploading assets to Webdam'),
'init_message' => $this->t('Initializing upload.'),
'progress_message' => $this->t('Processing (@percentage)...'),
'operations' => [],
'finished' => [static::class, 'batchFinish'],
];
foreach ((array) $form_state->getValue(['upload', 'uploaded_files'], []) as $file) {
$batch['operations'][] = [
[static::class, 'batchUploadFiles'],
[
$file,
$form_state->getValue('folder'),
],
];
}
foreach ((array) $form_state->getValue(['upload', 'uploaded_files'], []) as $file) {
$batch['operations'][] = [
[static::class, 'batchCreateEntities'],
[
$file,
$type->get('source_configuration')['source_field'],
$type->id(),
],
];
}
// Batch redirect callback needs UUID so we save it into the session.
$this->session->set('webdam_upload_batch_uuid', $form_state->get(['entity_browser', 'instance_uuid']));
batch_set($batch);
// Now that the batch is set manually set source URL which will ensure
// that we persist all needed query arguments when redirected back to
// the form.
if (\Drupal::request()->query->count()) {
$batch = &batch_get();
$source_url = Url::fromRouteMatch(\Drupal::routeMatch());
$source_url->setOption('query', \Drupal::request()->query->all());
$batch['source_url'] = $source_url;
}
}
else {
if (!$type) {
(new BundleNotExistException($this->configuration['media_type']))->logException()->displayMessage();
}
else {
(new BundleNotWebdamException($type->label()))->logException()->displayMessage();
}
}
}
elseif (!empty($form_state->getTriggeringElement()['#eb_widget_main_submit'])) {
try {
$media = $this->prepareEntities($form, $form_state);
array_walk($media, function (MediaInterface $item) {
if (!$item->id()) {
// Some race conditions might occur in some circumstances and could
// try to save this entity twice.
try {
$item->save();
}
catch (EntityStorageException $e) {
}
}
});
$this->selectEntities($media, $form_state);
$form_state->set('uploaded_assets', NULL);
$this->clearFormValues($element, $form_state);
}
catch (WebdamException $e) {
$e->displayMessage();
return;
}
}
}
/**
* Upload batch operation callback which uploads assets to Webdam.
*/
public static function batchUploadFiles($file, $folder, &$context) {
try {
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
/** @var \Bynder\webdam\Client $client */
$client = \Drupal::service('webdam.connector')->getClient();
$result = $client->uploadAsset(
$file_system->realpath($file['path']),
$file['filename'],
$folder
);
$context['results'][$file['path']] = $result;
$file_system->delete($file['path']);
$context['message'] = t('Uploaded @file to Webdam.', ['@file' => $file['filename']]);
}
catch (\Exception $e) {
// If fetching failed try few more times. If waiting doesn't help fail the
// batch eventually.
if (empty($context['sandbox']['fails'])) {
$context['sandbox']['fails'] = 0;
}
$context['sandbox']['fails']++;
$context['finished'] = 0;
$context['message'] = t('Uploading @file to Webdam.', ['@file' => $file['filename']]);
if ($context['sandbox']['fails'] >= static::FAIL_LIMIT) {
throw $e;
}
}
}
/**
* Upload batch operation callback which creates media entities.
*/
public static function batchCreateEntities($file, $source_field, $bundle, &$context) {
try {
// Let's try to fetch the uploaded resource from the API as we will be
// able to save it only if that succeeds.
$uuid = $context['results'][$file['path']];
// @todo We do not have any media info, because we cannot provide any.
/** @var \Drupal\webdam\WebdamConnector $webdam_connector */
$webdam_connector = \Drupal::service('webdam.connector');
$media_info = $webdam_connector->getMetadataGraphQL($uuid);
if (!$media_info) {
throw new \Exception('Unable to JSON encode the returned API response for the media UUID ' . $uuid);
}
$entity = Media::create([
'bundle' => $bundle,
$source_field => $uuid,
WebdamMetadataItem::METADATA_FIELD_NAME => $media_info,
]);
unset($context['results'][$file['path']]);
$context['results'][] = $entity;
$context['message'] = t('Mapped @file locally.', ['@file' => $file['filename']]);
}
catch (\Exception $e) {
// If fetching failed try few more times. If waiting doesn't help fail the
// batch eventually.
if (empty($context['sandbox']['fails'])) {
$context['sandbox']['fails'] = 0;
}
$context['sandbox']['fails']++;
$context['finished'] = 0;
$context['message'] = t('Mapping @file locally.', ['@file' => $file['filename']]);
sleep(3);
if ($context['sandbox']['fails'] >= static::FAIL_LIMIT) {
Error::logException(\Drupal::logger('webdam'), $e);
(new UploadFailedException(t("There was an unexpected error after uploading the file to Webdam.")))->displayMessage();
\Drupal::messenger()->addWarning(t('There was an unexpected error after uploading the file to Webdam. Please contact your site administrator for more info.'));
}
}
}
/**
* Upload batch finish callback.
*
* Stores results (media entities) into the session for the form to be able to
* pick them up.
*/
public static function batchFinish($success, $results, $operations) {
// Save results into the form state to make them available in the form.
\Drupal::service('session')->set('webdam_upload_batch_result', $results);
}
/**
* Clear values from upload form element.
*
* @param array $element
* Upload form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
*/
protected function clearFormValues(array &$element, FormStateInterface $form_state) {
$form_state->setValueForElement($element['upload']['uploaded_files'], '');
NestedArray::setValue($form_state->getUserInput(), $element['upload']['uploaded_files']['#parents'], '');
$form_state->set('uploaded_entities', NULL);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
foreach ($this->entityTypeManager->getStorage('media_type')->loadMultiple() as $type) {
/** @var \Drupal\media\MediaTypeInterface $type */
if ($type->getSource() instanceof Webdam) {
$form['media_type']['#options'][$type->id()] = $type->label();
}
}
if (empty($form['media_type']['#options'])) {
$form['media_type']['#disabled'] = TRUE;
$form['media_type']['#description'] = $this->t('You must @create_bundle before using this widget.', [
'@create_bundle' => Link::createFromRoute($this->t('create a Webdam media type'), 'media.bundle_add')->toString(),
]);
}
$form['extensions'] = [
'#type' => 'textfield',
'#title' => $this->t('Allowed file extensions'),
'#desciption' => $this->t('A space separated list of file extensions'),
'#default_value' => $this->configuration['extensions'],
];
$form['dropzone_description'] = [
'#type' => 'textfield',
'#title' => $this->t('Dropzone drag-n-drop zone text'),
'#default_value' => $this->configuration['dropzone_description'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validate(array &$form, FormStateInterface $form_state) {
try {
parent::validate($form, $form_state);
}
catch (WebdamException $e) {
$e->displayMessage();
return;
}
}
}
