diff --git a/composer.json b/composer.json index db04a3ba..610d4ae3 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,10 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "drupal/file_replace": "^1.1", - "islandora/crayfish-commons": "dev-dev" + "islandora/crayfish-commons": "dev-dev", + "drupal/hook_post_action" : "^1.0", + "drupal/file_replace": "^1.1" + }, "require-dev": { "phpunit/phpunit": "^6", diff --git a/islandora.info.yml b/islandora.info.yml index f5da3c93..9d5bc8a4 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -31,4 +31,5 @@ dependencies: - content_translation - flysystem - token + - hook_post_action - file_replace diff --git a/islandora.module b/islandora.module index 76f887e8..88db562d 100644 --- a/islandora.module +++ b/islandora.module @@ -107,7 +107,6 @@ function islandora_node_delete(NodeInterface $node) { */ function islandora_media_insert(MediaInterface $media) { $utils = \Drupal::service('islandora.utils'); - // Execute index reactions. $utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\IndexReaction', $media); @@ -137,7 +136,6 @@ function islandora_media_update(MediaInterface $media) { // Execute index reactions. $utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\IndexReaction', $media); - // Does it have a source field? $source_field = $media_source_service->getSourceFieldName($media->bundle()); if (empty($source_field)) { @@ -148,7 +146,6 @@ function islandora_media_update(MediaInterface $media) { if ($media->get($source_field)->equals($media->original->get($source_field))) { return; } - // If it has a parent node... $node = $utils->getParentNode($media); if ($node) { @@ -158,6 +155,7 @@ function islandora_media_update(MediaInterface $media) { $node, $media ); + $utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\DerivativeFileReaction', $media); } } @@ -171,6 +169,19 @@ function islandora_media_delete(MediaInterface $media) { $utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\DeleteReaction', $media); } +/** + * Implements hook_ENTITYTYPE_postsave(). + */ +function islandora_media_postsave(EntityInterface $media, $op) { + + $utils = \Drupal::service('islandora.utils'); + // Add derived file to the media. + if ($op == 'insert') { + $utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\DerivativeFileReaction', $media); + } + +} + /** * Implements hook_file_insert(). */ @@ -274,10 +285,11 @@ function islandora_jsonld_alter_normalized_array(EntityInterface $entity, array function islandora_entity_view_mode_alter(&$view_mode, EntityInterface $entity) { // Change the view mode based on user input from a 'view_mode_alter' // ContextReaction. + $entity_type = $entity->getEntityType()->id(); $storage = \Drupal::service('entity_type.manager')->getStorage('entity_view_mode'); $context_manager = \Drupal::service('context.manager'); - $current_entity = \Drupal::routeMatch()->getParameter('node'); - $current_id = ($current_entity instanceof NodeInterface) ? $current_entity->id() : NULL; + $current_entity = \Drupal::routeMatch()->getParameter($entity_type); + $current_id = ($current_entity instanceof NodeInterface || $current_entity instanceof MediaInterface) ? $current_entity->id() : NULL; if (isset($current_id) && $current_id == $entity->id()) { foreach ($context_manager->getActiveReactions('\Drupal\islandora\Plugin\ContextReaction\ViewModeAlterReaction') as $reaction) { // Construct the new view mode's machine name. diff --git a/islandora.routing.yml b/islandora.routing.yml index 5ca9e348..7ceacf7b 100644 --- a/islandora.routing.yml +++ b/islandora.routing.yml @@ -58,6 +58,20 @@ islandora.media_source_put_to_node: options: _auth: ['basic_auth', 'cookie', 'jwt_auth'] +islandora.attach_file_to_media: + path: '/media/add_derivative/{media}/{destination_field}' + defaults: + _controller: '\Drupal\islandora\Controller\MediaSourceController::attachToMedia' + methods: [GET, PUT] + requirements: + _custom_access: '\Drupal\islandora\Controller\MediaSourceController::attachToMediaAccess' + options: + _auth: ['basic_auth', 'cookie', 'jwt_auth'] + no_cache: 'TRUE' + parameters: + media: + type: entity:media + islandora.confirm_delete_media_and_file: path: '/media/delete_with_files' defaults: @@ -65,3 +79,4 @@ islandora.confirm_delete_media_and_file: requirements: _permission: 'administer media+delete any media' + diff --git a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php new file mode 100644 index 00000000..b73e163e --- /dev/null +++ b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php @@ -0,0 +1,43 @@ +fileSystem = $fileSystem; + } + + /** + * Controller's create method for dependency injection. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The App Container. + * + * @return \Drupal\islandora\Controller\MediaSourceController + * Controller instance. + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('file_system') + ); + } + + /** + * Attaches incoming file to existing media. + * + * @param \Drupal\media\Entity\Media $media + * Media to hold file. + * @param string $destination_field + * Media field to hold file. + * @param string $destination_text_field + * Media field to hold extracted text. + * @param \Symfony\Component\HttpFoundation\Request $request + * HTTP Request from Karaf. + * + * @return \Symfony\Component\HttpFoundation\Response + * HTTP response. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * @throws \Drupal\Core\TypedData\Exception\ReadOnlyException + */ + public function attachToMedia( + Media $media, + string $destination_field, + string $destination_text_field, + Request $request) { + $content_location = $request->headers->get('Content-Location', ""); + $contents = $request->getContent(); + + if ($contents) { + $directory = $this->fileSystem->dirname($content_location); + if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { + throw new HttpException(500, "The destination directory does not exist, could not be created, or is not writable"); + } + $file = file_save_data($contents, $content_location, FILE_EXISTS_REPLACE); + if ($media->hasField($destination_field)) { + $media->{$destination_field}->setValue([ + 'target_id' => $file->id(), + ]); + } + else { + $this->getLogger('islandora')->warning("Field $destination_field is not defined in Media Type {$media->bundle()}"); + } + if ($media->hasField($destination_text_field)) { + $media->{$destination_text_field}->setValue(nl2br($contents)); + } + else { + $this->getLogger('islandora')->warning("Field $destination_text_field is not defined in Media Type {$media->bundle()}"); + } + $media->save(); + } + // We'd only ever get here if testing the function with GET. + return new Response("

Complete

"); + } + +} diff --git a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php new file mode 100644 index 00000000..2c5cbcce --- /dev/null +++ b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php @@ -0,0 +1,103 @@ +entityFieldManager->getFieldMapByFieldType('text_long'); + $file_fields = $map['media']; + $field_options = array_combine(array_keys($file_fields), array_keys($file_fields)); + $form = parent::buildConfigurationForm($form, $form_state); + $form['mimetype']['#description'] = t('Mimetype to convert to (e.g. application/xml, etc...)'); + $form['mimetype']['#value'] = 'text/plain'; + $form['mimetype']['#type'] = 'hidden'; + $position = array_search('destination_field_name', array_keys($form)); + $first = array_slice($form, 0, $position); + $last = array_slice($form, count($form) - $position + 1); + + $middle['destination_text_field_name'] = [ + '#required' => TRUE, + '#type' => 'select', + '#options' => $field_options, + '#title' => $this->t('Destination Text field Name'), + '#default_value' => $this->configuration['destination_text_field_name'], + '#description' => $this->t('Text field on Media Type to hold extracted text.'), + ]; + $form = array_merge($first, $middle, $last); + + unset($form['args']); + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $form_state); + $exploded_mime = explode('/', $form_state->getValue('mimetype')); + if ($exploded_mime[0] != 'text') { + $form_state->setErrorByName( + 'mimetype', + t('Please enter file mimetype (e.g. application/xml.)') + ); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['destination_text_field_name'] = $form_state->getValue('destination_text_field_name'); + } + + /** + * Override this to return arbitrary data as an array to be json encoded. + */ + protected function generateData(EntityInterface $entity) { + $data = parent::generateData($entity); + $route_params = [ + 'media' => $entity->id(), + 'destination_field' => $this->configuration['destination_field_name'], + 'destination_text_field' => $this->configuration['destination_text_field_name'], + ]; + $data['destination_uri'] = Url::fromRoute('islandora_text_extraction.attach_file_to_media', $route_params) + ->setAbsolute() + ->toString(); + + return $data; + } + +} diff --git a/src/Controller/MediaSourceController.php b/src/Controller/MediaSourceController.php index 2e5b4b26..374d0f8b 100644 --- a/src/Controller/MediaSourceController.php +++ b/src/Controller/MediaSourceController.php @@ -7,6 +7,7 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Database\Connection; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Session\AccountInterface; +use Drupal\media\Entity\Media; use Drupal\media\MediaInterface; use Drupal\media\MediaTypeInterface; use Drupal\node\NodeInterface; @@ -210,4 +211,59 @@ class MediaSourceController extends ControllerBase { return AccessResult::allowedIf($node->access('update', $account) && $account->hasPermission('create media')); } + /** + * Adds file to existing media. + * + * @param Drupal\media\Entity\Media\Media $media + * The media to which file is added. + * @param string $destination_field + * The name of the media field to add file reference. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return \Symfony\Component\HttpFoundation\Response + * 201 on success with a Location link header. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * @throws \Drupal\Core\TypedData\Exception\ReadOnlyException + */ + public function attachToMedia( + Media $media, + string $destination_field, + Request $request) { + $content_location = $request->headers->get('Content-Location', ""); + $contents = $request->getContent(); + if ($contents) { + $file = file_save_data($contents, $content_location, FILE_EXISTS_REPLACE); + if ($media->hasField($destination_field)) { + $media->{$destination_field}->setValue([ + 'target_id' => $file->id(), + ]); + $media->save(); + } + else { + $this->getLogger('islandora')->warning("Field $destination_field is not defined in Media Type {$media->bundle()}"); + } + } + // Should only see this with a GET request for testing. + return new Response("

Complete

"); + } + + /** + * Checks for permissions to update a node and update media. + * + * @param \Drupal\Core\Session\AccountInterface $account + * Account for user making the request. + * @param \Drupal\Core\Routing\RouteMatch $route_match + * Route match to get Node from url params. + * + * @return \Drupal\Core\Access\AccessResultInterface + * Access result. + */ + public function attachToMediaAccess(AccountInterface $account, RouteMatch $route_match) { + $media = $route_match->getParameter('media'); + $node = $this->utils->getParentNode($media); + return AccessResult::allowedIf($node->access('update', $account) && $account->hasPermission('create media')); + } + } diff --git a/src/Form/ConfirmDeleteMediaAndFile.php b/src/Form/ConfirmDeleteMediaAndFile.php index 85b01209..04b7d458 100644 --- a/src/Form/ConfirmDeleteMediaAndFile.php +++ b/src/Form/ConfirmDeleteMediaAndFile.php @@ -2,6 +2,7 @@ namespace Drupal\islandora\Form; +use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Form\DeleteMultipleForm; use Drupal\Core\Form\FormStateInterface; @@ -9,6 +10,7 @@ use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TempStore\PrivateTempStoreFactory; use Drupal\Core\Url; +use Drupal\file\Entity\File; use Drupal\islandora\MediaSource\MediaSourceService; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -39,12 +41,20 @@ class ConfirmDeleteMediaAndFile extends DeleteMultipleForm { */ protected $selection = []; + /** + * Entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + /** * {@inheritdoc} */ - public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger, MediaSourceService $media_source_service, LoggerInterface $logger) { + public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger, MediaSourceService $media_source_service, LoggerInterface $logger) { $this->currentUser = $current_user; $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; $this->tempStore = $temp_store_factory->get('media_and_file_delete_confirm'); $this->messenger = $messenger; $this->mediaSourceService = $media_source_service; @@ -58,6 +68,7 @@ class ConfirmDeleteMediaAndFile extends DeleteMultipleForm { return new static( $container->get('current_user'), $container->get('entity_type.manager'), + $container->get('entity_field.manager'), $container->get('tempstore.private'), $container->get('messenger'), $container->get('islandora.media_source_service'), @@ -115,15 +126,24 @@ class ConfirmDeleteMediaAndFile extends DeleteMultipleForm { continue; } // Check for files. - $source_field = $this->mediaSourceService->getSourceFieldName($entity->bundle()); - foreach ($entity->get($source_field)->referencedEntities() as $file) { - if (!$file->access('delete', $this->currentUser)) { - $inaccessible_entities[] = $file; - continue; + $fields = $this->entityFieldManager->getFieldDefinitions('media', $entity->bundle()); + $files = []; + foreach ($fields as $field) { + $type = $field->getType(); + if ($type == 'file' || $type == 'image') { + $target_id = $entity->get($field->getName())->target_id; + $file = File::load($target_id); + if ($file) { + if (!$file->access('delete', $this->currentUser)) { + $inaccessible_entities[] = $file; + continue; + } + $delete_files[$file->id()] = $file; + $total_count++; + } } - $delete_files[$file->id()] = $file; - $total_count++; } + foreach ($selected_langcodes as $langcode) { // We're only working with media, which are translatable. $entity = $entity->getTranslation($langcode); diff --git a/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php new file mode 100644 index 00000000..151af915 --- /dev/null +++ b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php @@ -0,0 +1,316 @@ +utils = $utils; + $this->mediaSource = $media_source; + $this->token = $token; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('islandora.eventgenerator'), + $container->get('islandora.stomp'), + $container->get('jwt.authentication.jwt'), + $container->get('islandora.utils'), + $container->get('islandora.media_source_service'), + $container->get('token'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $uri = 'http://pcdm.org/use#OriginalFile'; + return [ + 'queue' => 'islandora-connector-houdini', + 'event' => 'Generate Derivative', + 'source_term_uri' => $uri, + 'mimetype' => '', + 'args' => '', + 'path' => '[date:custom:Y]-[date:custom:m]/[media:mid].bin', + 'source_field_name' => 'field_media_file', + 'destination_field_name' => '', + ]; + } + + /** + * Override this to return arbitrary data as an array to be json encoded. + */ + protected function generateData(EntityInterface $entity) { + $data = parent::generateData($entity); + if (get_class($entity) != 'Drupal\media\Entity\Media') { + return; + } + $source_file = $this->mediaSource->getSourceFile($entity); + if (!$source_file) { + throw new \RuntimeException("Could not locate source file for media {$entity->id()}", 500); + } + $data['source_uri'] = $this->utils->getDownloadUrl($source_file); + + $route_params = [ + 'media' => $entity->id(), + 'destination_field' => $this->configuration['destination_field_name'], + ]; + $data['destination_uri'] = Url::fromRoute('islandora.attach_file_to_media', $route_params) + ->setAbsolute() + ->toString(); + + $token_data = [ + 'media' => $entity, + ]; + $destination_field = $this->configuration['destination_field_name']; + $field = \Drupal::entityTypeManager() + ->getStorage('field_storage_config') + ->load("media.$destination_field"); + $scheme = $field->getSetting('uri_scheme'); + $path = $this->token->replace($data['path'], $token_data); + $data['file_upload_uri'] = $scheme . '://' . $path; + $allowed = [ + 'queue', + 'event', + 'args', + 'source_uri', + 'destination_uri', + 'file_upload_uri', + 'mimetype', + ]; + foreach ($data as $key => $value) { + if (!in_array($key, $allowed)) { + unset($data[$key]); + } + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + $map = $this->entityFieldManager->getFieldMapByFieldType('file'); + $file_fields = $map['media']; + $file_options = array_combine(array_keys($file_fields), array_keys($file_fields)); + $file_options = array_merge(['' => ''], $file_options); + $form['event']['#disabled'] = 'disabled'; + + $form['destination_field_name'] = [ + '#required' => TRUE, + '#type' => 'select', + '#options' => $file_options, + '#title' => $this->t('Destination File field Name'), + '#default_value' => $this->configuration['destination_field_name'], + '#description' => $this->t('File field on Media Type to hold derivative. Cannot be the same as source'), + ]; + + $form['args'] = [ + '#type' => 'textfield', + '#title' => t('Additional arguments'), + '#default_value' => $this->configuration['args'], + '#rows' => '8', + '#description' => t('Additional command line arguments'), + ]; + + $form['mimetype'] = [ + '#type' => 'textfield', + '#title' => t('Mimetype'), + '#default_value' => $this->configuration['mimetype'], + '#required' => TRUE, + '#rows' => '8', + '#description' => t('Mimetype to convert to (e.g. image/jpeg, video/mp4, etc...)'), + ]; + + $form['path'] = [ + '#type' => 'textfield', + '#title' => t('File path'), + '#default_value' => $this->configuration['path'], + '#description' => t('Path within the upload destination where files will be stored. Includes the filename and optional extension.'), + ]; + $form['queue'] = [ + '#type' => 'textfield', + '#title' => t('Queue name'), + '#default_value' => $this->configuration['queue'], + '#description' => t('Queue name to send along to help routing events, CHANGE WITH CARE. Defaults to :queue', [ + ':queue' => $this->defaultConfiguration()['queue'], + ]), + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $form_state); + $mimetype = $form_state->getValue('mimetype'); + $exploded = explode('/', $form_state->getValue('mimetype')); + if (count($exploded) != 2) { + $form_state->setErrorByName( + 'mimetype', + t('Please enter a mimetype (e.g. image/jpeg, video/mp4, audio/mp3, etc...)') + ); + } + + if (empty($exploded[1])) { + $form_state->setErrorByName( + 'mimetype', + t('Please enter a mimetype (e.g. image/jpeg, video/mp4, audio/mp3, etc...)') + ); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['mimetype'] = $form_state->getValue('mimetype'); + $this->configuration['args'] = $form_state->getValue('args'); + $this->configuration['scheme'] = $form_state->getValue('scheme'); + $this->configuration['path'] = trim($form_state->getValue('path'), '\\/'); + $this->configuration['destination_field_name'] = $form_state->getValue('destination_field_name'); + } + + /** + * Find a media_type by id and return it or nothing. + * + * @param string $entity_id + * The media type. + * + * @return \Drupal\Core\Entity\EntityInterface|string + * Return the loaded entity or nothing. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * Thrown by getStorage() if the entity type doesn't exist. + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * Thrown by getStorage() if the storage handler couldn't be loaded. + */ + protected function getEntityById($entity_id) { + $entity_ids = $this->entityTypeManager->getStorage('media_type') + ->getQuery()->condition('id', $entity_id)->execute(); + + $id = reset($entity_ids); + if ($id !== FALSE) { + return $this->entityTypeManager->getStorage('media_type')->load($id); + } + return ''; + } + +} diff --git a/src/Plugin/Condition/MediaSourceHasMimetype.php b/src/Plugin/Condition/MediaSourceHasMimetype.php new file mode 100644 index 00000000..ba16bb35 --- /dev/null +++ b/src/Plugin/Condition/MediaSourceHasMimetype.php @@ -0,0 +1,89 @@ + $this->t('Source Media Mimetype'), + '#type' => 'textfield', + '#default_value' => $this->configuration['mimetype'], + ]; + + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['mimetype'] = $form_state->getValue('mimetype'); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + foreach ($this->getContexts() as $context) { + if ($context->hasContextValue()) { + $entity = $context->getContextValue(); + $mid = $entity->id(); + if ($mid && !empty($this->configuration['mimetype'])) { + $source = $entity->getSource(); + $source_file = File::load($source->getSourceFieldValue($entity)); + if ($this->configuration['mimetype'] == $source_file->getMimeType()) { + return !$this->isNegated(); + } + } + } + } + return $this->isNegated(); + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (empty($this->configuration['mimetype'])) { + return $this->t('No mimetype are selected.'); + } + + return $this->t( + 'Entity bundle in the list: @mimetype', + [ + '@mimetype' => implode(', ', $this->configuration['field']), + ] + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array_merge( + ['mimetype' => []], + parent::defaultConfiguration() + ); + } + +} diff --git a/src/Plugin/ContextReaction/DerivativeFileReaction.php b/src/Plugin/ContextReaction/DerivativeFileReaction.php new file mode 100644 index 00000000..c0562fa6 --- /dev/null +++ b/src/Plugin/ContextReaction/DerivativeFileReaction.php @@ -0,0 +1,58 @@ +actionStorage->loadByProperties(['type' => 'media']); + + foreach ($actions as $action) { + $plugin = $action->getPlugin(); + if ($plugin instanceof AbstractGenerateDerivativeMediaFile) { + $options[ucfirst($action->getType())][$action->id()] = $action->label(); + } + } + $config = $this->getConfiguration(); + $form['actions'] = [ + '#title' => $this->t('Actions'), + '#description' => $this->t('Pre-configured actions to execute. Multiple actions may be selected by shift or ctrl clicking.'), + '#type' => 'select', + '#multiple' => TRUE, + '#options' => $options, + '#default_value' => isset($config['actions']) ? $config['actions'] : '', + '#size' => 15, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function execute(EntityInterface $entity = NULL) { + $config = $this->getConfiguration(); + $action_ids = $config['actions']; + foreach ($action_ids as $action_id) { + $action = $this->actionStorage->load($action_id); + $action->execute([$entity]); + } + } + +}