diff --git a/islandora.info.yml b/islandora.info.yml index 0e0a265e..bf13796e 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -31,3 +31,4 @@ dependencies: - content_translation - flysystem - token + - hook_post_action diff --git a/islandora.module b/islandora.module index 3d415094..c61becb3 100644 --- a/islandora.module +++ b/islandora.module @@ -106,7 +106,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); @@ -136,7 +135,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)) { @@ -147,7 +145,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) { @@ -157,6 +154,7 @@ function islandora_media_update(MediaInterface $media) { $node, $media ); + $utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\DerivativeFileReaction', $media); } } @@ -170,6 +168,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(). */ diff --git a/islandora.permissions.yml b/islandora.permissions.yml index 3ea34595..e69de29b 100644 --- a/islandora.permissions.yml +++ b/islandora.permissions.yml @@ -1,12 +0,0 @@ - -view checksums: - title: 'View Checksums' - description: 'Allows access to viewing file checksums' - -manage members: - title: 'Manage Members' - description: 'Allows access to managing members for content' - -manage media: - title: 'Manage Media' - description: 'Allows access to managing media for content' diff --git a/islandora.routing.yml b/islandora.routing.yml index 58dea1a2..ddfd93f1 100644 --- a/islandora.routing.yml +++ b/islandora.routing.yml @@ -57,3 +57,17 @@ islandora.media_source_put_to_node: _custom_access: '\Drupal\islandora\Controller\MediaSourceController::putToNodeAccess' 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 diff --git a/modules/islandora_audio/config/schema/islandora_audio.info.yml b/modules/islandora_audio/config/schema/islandora_audio.info.yml new file mode 100644 index 00000000..47d08ee3 --- /dev/null +++ b/modules/islandora_audio/config/schema/islandora_audio.info.yml @@ -0,0 +1,28 @@ +action.configuration.generate_audio_derivative: + type: mapping + label: 'Generate a audio derivative...' + mapping: + queue: + type: text + label: 'Queue' + event: + type: text + label: 'Event Type' + source_term_uri: + type: text + label: 'Source term uri' + derivative_term_uri: + type: text + label: 'Destination term uri' + mimetype: + type: text + label: 'Audio Mimetype' + args: + type: text + label: 'FFMpeg Arguments' + scheme: + type: text + label: 'Flysystem scheme' + path: + type: text + label: 'File path with extension' diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml index 58e3c959..e69de29b 100644 --- a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml @@ -1,6 +0,0 @@ -services: - islandora_breadcrumbs.breadcrumb: - class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder - arguments: ['@entity_type.manager', '@config.factory'] - tags: - - { name: breadcrumb_builder, priority: 100 } diff --git a/modules/islandora_image/config/schema/islandora_image.info.yml b/modules/islandora_image/config/schema/islandora_image.info.yml new file mode 100644 index 00000000..350516d0 --- /dev/null +++ b/modules/islandora_image/config/schema/islandora_image.info.yml @@ -0,0 +1,28 @@ +action.configuration.generate_image_derivative: + type: mapping + label: 'Generate an image derivative...' + mapping: + queue: + type: text + label: 'Queue' + event: + type: text + label: 'Event Type' + source_term_uri: + type: text + label: 'Source term uri' + derivative_term_uri: + type: text + label: 'Destination term uri' + mimetype: + type: text + label: 'Image Mimetype' + args: + type: text + label: 'Convert Arguments' + scheme: + type: text + label: 'Flysystem scheme' + path: + type: text + label: 'File path with extension' 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 @@ +getSettings(); + $extensions = $fieldSettings['file_extensions']; + if (!strpos($extensions, 'txt')) { + $fieldSettings['file_extensions'] .= ' txt'; + $field->set('settings', $fieldSettings); + $field->save(); + } +} diff --git a/modules/islandora_text_extraction/islandora_text_extraction.routing.yml b/modules/islandora_text_extraction/islandora_text_extraction.routing.yml new file mode 100644 index 00000000..e28a056b --- /dev/null +++ b/modules/islandora_text_extraction/islandora_text_extraction.routing.yml @@ -0,0 +1,13 @@ +islandora_text_extraction.attach_file_to_media: + path: '/media/add_ocr/{media}/{destination_field}/{destination_text_field}' + defaults: + _controller: '\Drupal\islandora_text_extraction\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 diff --git a/modules/islandora_text_extraction/src/Controller/MediaSourceController.php b/modules/islandora_text_extraction/src/Controller/MediaSourceController.php new file mode 100644 index 00000000..efa0a557 --- /dev/null +++ b/modules/islandora_text_extraction/src/Controller/MediaSourceController.php @@ -0,0 +1,54 @@ +headers->get('Content-Location', ""); + $contents = $request->getContent(); + + if ($contents) { + \Drupal::logger('Alan_dev')->warning("Content location is $content_location"); + $file = file_save_data($contents, $content_location, FILE_EXISTS_REPLACE); + $media->{$destination_field}->setValue([ + 'target_id' => $file->id(), + ]); + $media->{$destination_text_field}->setValue(nl2br($contents)); + $media->save(); + + 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/modules/islandora_video/config/schema/islandora_video.info.yml b/modules/islandora_video/config/schema/islandora_video.info.yml new file mode 100644 index 00000000..2d54e4b0 --- /dev/null +++ b/modules/islandora_video/config/schema/islandora_video.info.yml @@ -0,0 +1,28 @@ +action.configuration.generate_video_derivative: + type: mapping + label: 'Generate a video derivative...' + mapping: + queue: + type: text + label: 'Queue' + event: + type: text + label: 'Event Type' + source_term_uri: + type: text + label: 'Source term uri' + derivative_term_uri: + type: text + label: 'Destination term uri' + mimetype: + type: text + label: 'Video Mimetype' + args: + type: text + label: 'FFMpeg Arguments' + scheme: + type: text + label: 'Flysystem scheme' + path: + type: text + label: 'File path with extension' diff --git a/src/Controller/MediaSourceController.php b/src/Controller/MediaSourceController.php index 2e5b4b26..269b56a6 100644 --- a/src/Controller/MediaSourceController.php +++ b/src/Controller/MediaSourceController.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; +use Drupal\media\Entity\Media; /** * Class MediaSourceController. @@ -210,4 +211,53 @@ 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 + * 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); + $media->{$destination_field}->setValue([ + 'target_id' => $file->id(), + ]); + $media->save(); + } + 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/EventGenerator/EventGenerator.php b/src/EventGenerator/EventGenerator.php index 3175c224..ecc9fee9 100644 --- a/src/EventGenerator/EventGenerator.php +++ b/src/EventGenerator/EventGenerator.php @@ -156,7 +156,6 @@ class EventGenerator implements EventGeneratorInterface { "mediaType" => "application/json", ]; } - return json_encode($event); } diff --git a/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php new file mode 100644 index 00000000..9b20eb1e --- /dev/null +++ b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php @@ -0,0 +1,318 @@ +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' => '', + 'scheme' => file_default_scheme(), + '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, + ]; + $path = $this->token->replace($data['path'], $token_data); + $data['file_upload_uri'] = $data['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) { + $schemes = $this->utils->getFilesystemSchemes(); + $scheme_options = array_combine($schemes, $schemes); + $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['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['args'] = [ + '#type' => 'textfield', + '#title' => t('Additional arguments'), + '#default_value' => $this->configuration['args'], + '#rows' => '8', + '#description' => t('Additional command line arguments'), + ]; + $form['scheme'] = [ + '#type' => 'select', + '#title' => t('File system'), + '#options' => $scheme_options, + '#default_value' => $this->configuration['scheme'], + '#required' => TRUE, + ]; + $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); + + $exploded_mime = explode('/', $form_state->getValue('mimetype')); + + if (count($exploded_mime) != 2) { + $form_state->setErrorByName( + 'mimetype', + t('Please enter a mimetype (e.g. image/jpeg, video/mp4, audio/mp3, etc...)') + ); + } + + if (empty($exploded_mime[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/Action/DeleteMediaAndFile.php b/src/Plugin/Action/DeleteMediaAndFile.php index 2e9d1df1..eeea0cb1 100644 --- a/src/Plugin/Action/DeleteMediaAndFile.php +++ b/src/Plugin/Action/DeleteMediaAndFile.php @@ -9,6 +9,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\islandora\MediaSource\MediaSourceService; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\file\Entity\File; /** * Deletes a media and its source file. @@ -101,6 +102,15 @@ class DeleteMediaAndFile extends ActionBase implements ContainerFactoryPluginInt $source_field = $this->mediaSourceService->getSourceFieldName($entity->bundle()); foreach ($entity->get($source_field)->referencedEntities() as $file) { $file->delete(); + // Check for non-source fields; + foreach ($entity->getFieldDefinitions() as $field) { + if ($field->getType() == 'file') { + $fid = $entity->get($field->getName())->getValue()[0]['target_id']; + if ($fid) { + File::load($fid)->delete(); + } + } + } } $entity->delete(); } 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]); + } + } + +} diff --git a/src/Plugin/ContextReaction/DerivativeReaction.php b/src/Plugin/ContextReaction/DerivativeReaction.php index 7ddaed4f..d6d50988 100644 --- a/src/Plugin/ContextReaction/DerivativeReaction.php +++ b/src/Plugin/ContextReaction/DerivativeReaction.php @@ -2,6 +2,8 @@ namespace Drupal\islandora\Plugin\ContextReaction; +use Drupal\Core\Form\FormStateInterface; +use Drupal\islandora\Plugin\Action\AbstractGenerateDerivative; use Drupal\islandora\PresetReaction\PresetReaction; /** @@ -12,4 +14,32 @@ use Drupal\islandora\PresetReaction\PresetReaction; * label = @Translation("Derivative") * ) */ -class DerivativeReaction extends PresetReaction {} +class DerivativeReaction extends PresetReaction { + /** + * {@inheritdoc} + */ + + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $actions = $this->actionStorage->loadMultiple(); + $options = []; + foreach ($actions as $action) { + $plugin = $action->getPlugin(); + if ($plugin instanceof AbstractGenerateDerivative) { + $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; + } + +} diff --git a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php new file mode 100644 index 00000000..05f2f8c0 --- /dev/null +++ b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php @@ -0,0 +1,174 @@ +mediaSource = $media_source; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('config.factory'), + $container->get('islandora.utils'), + $container->get('islandora.media_source_service') + ); + } + + /** + * {@inheritdoc} + */ + public function summary() { + return $this->t('Map Drupal URI to configured predicate.'); + } + + /** + * {@inheritdoc} + */ + public function execute(EntityInterface $entity = NULL, array &$normalized = NULL, array $context = NULL) { + $config = $this->getConfiguration(); + $drupal_predicate = $config[self::URI_PREDICATE]; + if (!is_null($drupal_predicate) && !empty($drupal_predicate)) { + $url = $this->getSubjectUrl($entity); + if ($context['needs_jsonldcontext'] === FALSE) { + $drupal_predicate = NormalizerBase::escapePrefix($drupal_predicate, $context['namespaces']); + } + if (isset($normalized['@graph']) && is_array($normalized['@graph'])) { + foreach ($normalized['@graph'] as &$graph) { + if (isset($graph['@id']) && $graph['@id'] == $url) { + // Swap media and file urls. + if ($entity instanceof MediaInterface) { + $file = $this->mediaSource->getSourceFile($entity); + $graph['@id'] = $this->utils->getDownloadUrl($file); + } + if (isset($graph[$drupal_predicate])) { + if (!is_array($graph[$drupal_predicate])) { + if ($graph[$drupal_predicate] == $url) { + // Don't add it if it already exists. + return; + } + $tmp = $graph[$drupal_predicate]; + $graph[$drupal_predicate] = [$tmp]; + } + elseif (array_search($url, array_column($graph[$drupal_predicate], '@id'))) { + // Don't add it if it already exists. + return; + } + } + else { + $graph[$drupal_predicate] = []; + } + $graph[$drupal_predicate][] = ["@id" => $url]; + return; + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $config = $this->getConfiguration(); + $form[self::URI_PREDICATE] = [ + '#type' => 'textfield', + '#title' => $this->t('Drupal URI predicate'), + '#description' => $this->t("The Drupal object's URI will be added to the resource with this predicate. Must use a defined prefix."), + '#default_value' => isset($config[self::URI_PREDICATE]) ? $config[self::URI_PREDICATE] : '', + '#size' => 35, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $drupal_predicate = $form_state->getValue(self::URI_PREDICATE); + if (!is_null($drupal_predicate) and !empty($drupal_predicate)) { + if (preg_match('/^https?:\/\//', $drupal_predicate)) { + // Can't validate all URIs so we have to trust them. + return; + } + elseif (preg_match('/^([^\s:]+):/', $drupal_predicate, $matches)) { + $predicate_prefix = $matches[1]; + $rdf = rdf_get_namespaces(); + $rdf_prefixes = array_keys($rdf); + if (!in_array($predicate_prefix, $rdf_prefixes)) { + $form_state->setErrorByName( + self::URI_PREDICATE, + $this->t('Namespace prefix @prefix is not registered.', + ['@prefix' => $predicate_prefix] + ) + ); + } + } + else { + $form_state->setErrorByName( + self::URI_PREDICATE, + $this->t('Predicate must use a defined prefix or be a full URI') + ); + } + } + parent::validateConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->setConfiguration([self::URI_PREDICATE => $form_state->getValue(self::URI_PREDICATE)]); + } + +} diff --git a/tests/src/Functional/MappingUriPredicateReactionTest.php b/tests/src/Functional/MappingUriPredicateReactionTest.php new file mode 100644 index 00000000..bf7bff6e --- /dev/null +++ b/tests/src/Functional/MappingUriPredicateReactionTest.php @@ -0,0 +1,195 @@ + ['schema:dateCreated'], + 'datatype' => 'xsd:dateTime', + 'datatype_callback' => ['callable' => 'Drupal\rdf\CommonDataConverter::dateIso8601Value'], + ]; + + // Save bundle mapping config. + $this->rdfMapping = rdf_get_mapping('node', 'test_type') + ->setBundleMapping(['types' => $types]) + ->setFieldMapping('created', $created_mapping) + ->setFieldMapping('title', [ + 'properties' => ['dc:title'], + 'datatype' => 'xsd:string', + ]) + ->save(); + + $this->container->get('router.builder')->rebuildIfNeeded(); + } + + /** + * @covers \Drupal\islandora\Plugin\ContextReaction\MappingUriPredicateReaction + */ + public function testMappingReaction() { + $account = $this->drupalCreateUser([ + 'bypass node access', + 'administer contexts', + ]); + $this->drupalLogin($account); + + $context_name = 'test'; + $reaction_id = 'islandora_map_uri_predicate'; + + $this->postNodeAddForm('test_type', + ['title[0][value]' => 'Test Node'], + t('Save')); + $this->assertSession()->pageTextContains("Test Node"); + $url = $this->getUrl(); + + // Make sure the node exists. + $this->drupalGet($url); + $this->assertSession()->statusCodeEquals(200); + + $contents = $this->drupalGet($url . '?_format=jsonld'); + $this->assertSession()->statusCodeEquals(200); + $json = \GuzzleHttp\json_decode($contents, TRUE); + $this->assertArrayHasKey('http://purl.org/dc/terms/title', + $json['@graph'][0], 'Missing dcterms:title key'); + $this->assertEquals( + 'Test Node', + $json['@graph'][0]['http://purl.org/dc/terms/title'][0]['@value'], + 'Missing title value' + ); + $this->assertArrayNotHasKey('http://www.w3.org/2002/07/owl#sameAs', + $json['@graph'][0], 'Has predicate when not configured'); + + $this->createContext('Test', $context_name); + $this->drupalGet("admin/structure/context/$context_name/reaction/add/$reaction_id"); + $this->assertSession()->statusCodeEquals(200); + + $this->drupalGet("admin/structure/context/$context_name"); + // Can't use an undefined prefix. + $this->getSession()->getPage() + ->fillField("Drupal URI predicate", "bob:smith"); + $this->getSession()->getPage()->pressButton("Save and continue"); + $this->assertSession() + ->pageTextContains("Namespace prefix bob is not registered"); + + // Can't use a straight string. + $this->getSession()->getPage() + ->fillField("Drupal URI predicate", "woohoo"); + $this->getSession()->getPage()->pressButton("Save and continue"); + $this->assertSession() + ->pageTextContains("Predicate must use a defined prefix or be a full URI"); + + // Use an existing prefix. + $this->getSession()->getPage() + ->fillField("Drupal URI predicate", "owl:sameAs"); + $this->getSession()->getPage()->pressButton("Save and continue"); + $this->assertSession() + ->pageTextContains("The context $context_name has been saved"); + + $new_contents = $this->drupalGet($url . '?_format=jsonld'); + $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $this->assertEquals( + 'Test Node', + $json['@graph'][0]['http://purl.org/dc/terms/title'][0]['@value'], + 'Missing title value' + ); + $this->assertEquals( + "$url?_format=jsonld", + $json['@graph'][0]['http://www.w3.org/2002/07/owl#sameAs'][0]['@id'], + 'Missing alter added predicate.' + ); + + $this->drupalGet("admin/structure/context/$context_name"); + // Change to a random URL. + $this->getSession()->getPage() + ->fillField("Drupal URI predicate", "http://example.org/first/second"); + $this->getSession()->getPage()->pressButton("Save and continue"); + $this->assertSession() + ->pageTextContains("The context $context_name has been saved"); + $new_contents = $this->drupalGet($url . '?_format=jsonld'); + $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $this->assertEquals( + 'Test Node', + $json['@graph'][0]['http://purl.org/dc/terms/title'][0]['@value'], + 'Missing title value' + ); + $this->assertArrayNotHasKey('http://www.w3.org/2002/07/owl#sameAs', + $json['@graph'][0], 'Still has old predicate'); + $this->assertEquals( + "$url?_format=jsonld", + $json['@graph'][0]['http://example.org/first/second'][0]['@id'], + 'Missing alter added predicate.' + ); + } + + /** + * @covers \Drupal\islandora\Plugin\ContextReaction\MappingUriPredicateReaction + */ + public function testMappingReactionForMedia() { + $account = $this->drupalCreateUser([ + 'create media', + 'view media', + 'administer contexts', + ]); + $this->drupalLogin($account); + + $context_name = 'test'; + $reaction_id = 'islandora_map_uri_predicate'; + + list($file, $media) = $this->makeMediaAndFile($account); + $media_url = $media->url('canonical', ['absolute' => TRUE]); + $file_url = $file->url('canonical', ['absolute' => TRUE]); + + $this->drupalGet($media_url); + $this->assertSession()->statusCodeEquals(200); + + $contents = $this->drupalGet($media_url . '?_format=jsonld'); + $this->assertSession()->statusCodeEquals(200); + $json = \GuzzleHttp\json_decode($contents, TRUE); + $this->assertEquals( + "$media_url?_format=jsonld", + $json['@graph'][0]['@id'], + 'Swapped file and media urls when not configured' + ); + $this->assertArrayNotHasKey('http://www.iana.org/assignments/relation/describedby', + $json['@graph'][0], 'Has predicate when not configured'); + + $this->createContext('Test', $context_name); + $this->drupalGet("admin/structure/context/$context_name/reaction/add/$reaction_id"); + $this->assertSession()->statusCodeEquals(200); + + // Use an existing prefix. + $this->getSession()->getPage() + ->fillField("Drupal URI predicate", "iana:describedby"); + $this->getSession()->getPage()->pressButton("Save and continue"); + $this->assertSession() + ->pageTextContains("The context $context_name has been saved"); + + $new_contents = $this->drupalGet($media_url . '?_format=jsonld'); + $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $this->assertEquals( + "$media_url?_format=jsonld", + $json['@graph'][0]['http://www.iana.org/assignments/relation/describedby'][0]['@id'], + 'Missing alter added predicate.' + ); + $this->assertEquals( + $file_url, + $json['@graph'][0]['@id'], + 'Alter did not swap "@id" of media with file url.' + ); + + } + +}