diff --git a/composer.json b/composer.json index c02d7c16..0b443c02 100644 --- a/composer.json +++ b/composer.json @@ -14,22 +14,22 @@ } ], "require": { - "drupal/context": "^4.0@beta", - "drupal/search_api": "~1.8", - "islandora/jsonld": "^2", - "stomp-php/stomp-php": "4.* || ^5", - "drupal/jwt": "^1.0.0-beta5", - "drupal/filehash": "^1.1 || ^2", - "drupal/prepopulate" : "^2.2", - "drupal/eva" : "^2.0", + "drupal/context": "^4", + "drupal/ctools": "^3.8 || ^4", + "drupal/eva" : "^3.0", "drupal/features" : "^3.7", - "drupal/migrate_plus" : "^5.1", + "drupal/file_replace": "^1.1", + "drupal/filehash": "^2", + "drupal/flysystem" : "^2.0@alpha", + "drupal/jwt": "^1.0", + "drupal/migrate_plus" : "^5.1 || ^6", "drupal/migrate_source_csv" : "^3.4", + "drupal/prepopulate" : "^2.2", + "drupal/search_api": "^1.8", "drupal/token" : "^1.3", - "drupal/flysystem" : "^2.0@alpha", "islandora/crayfish-commons": "^2", - "drupal/file_replace": "^1.1", - "drupal/ctools": "^3.8 || ^4" + "islandora/jsonld": "^2", + "stomp-php/stomp-php": "4.* || ^5" }, "require-dev": { "phpunit/phpunit": "^6", diff --git a/config/install/islandora.settings.yml b/config/install/islandora.settings.yml index 8fb25fb8..179d4213 100644 --- a/config/install/islandora.settings.yml +++ b/config/install/islandora.settings.yml @@ -1,4 +1,5 @@ broker_url: 'tcp://localhost:61613' jwt_expiry: '+2 hour' gemini_url: '' +delete_media_and_files: TRUE gemini_pseudo_bundles: [] diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index e85d5739..86b65fd0 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -14,6 +14,9 @@ islandora.settings: jwt_expiry: type: string label: 'How long JWTs should last before expiring.' + delete_media_and_files: + type: boolean + label: 'Node Delete with Media and Files' upload_form_location: type: string label: 'Upload Form Location' @@ -166,7 +169,7 @@ condition.plugin.node_had_namespace: pid_field: type: ignore label: 'PID field' - + field.formatter.settings.islandora_image: type: field.formatter.settings.image label: 'Islandora image field display format settings' diff --git a/css/islandora.css b/css/islandora.css new file mode 100644 index 00000000..696587ac --- /dev/null +++ b/css/islandora.css @@ -0,0 +1,3 @@ +.container .islandora-media-items { + margin: 0; +} diff --git a/islandora.libraries.yml b/islandora.libraries.yml new file mode 100644 index 00000000..840dc294 --- /dev/null +++ b/islandora.libraries.yml @@ -0,0 +1,5 @@ +islandora: + version: VERSION + css: + theme: + css/islandora.css: {} diff --git a/islandora.module b/islandora.module index 5b5446d1..69176714 100644 --- a/islandora.module +++ b/islandora.module @@ -27,6 +27,8 @@ use Drupal\file\FileInterface; use Drupal\taxonomy\TermInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\serialization\Normalizer\CacheableNormalizerInterface; +use Drupal\Core\Entity\EntityForm; +use Drupal\file\Entity\File; /** * Implements hook_help(). @@ -181,7 +183,8 @@ function islandora_file_insert(FileInterface $file) { */ function islandora_file_update(FileInterface $file) { // Exit early if unchanged. - if ($file->filehash != NULL && $file->original->filehash != NULL && $file->filehash['sha1'] == $file->original->filehash['sha1']) { + if ($file->hasField('sha1') && $file->original->hasField('sha1') + && $file->sha1->getString() == $file->original->sha1->getString()) { return; } @@ -331,6 +334,197 @@ function islandora_form_alter(&$form, FormStateInterface $form_state, $form_id) } } } + + $form_object = $form_state->getFormObject(); + $utils = \Drupal::service('islandora.utils'); + $config = \Drupal::config('islandora.settings')->get('delete_media_and_files'); + + if ($config == 1 && $form_object instanceof EntityForm) { + $entity = $form_object->getEntity(); + + if ($entity->getEntityTypeId() == "node" && $utils->isIslandoraType($entity->getEntityTypeId(), $entity->bundle()) && strpos($form['#form_id'], 'delete_form') !== FALSE) { + $medias = $utils->getMedia($form_state->getFormObject()->getEntity()); + if (count($medias) != 0) { + $form['delete_associated_content'] = [ + '#type' => 'checkbox', + '#title' => t('Delete all associated medias and nodes'), + ]; + + $media_list = []; + + foreach ($medias as $media) { + $media_list[] = $media->getName(); + } + + $form['container'] = [ + '#type' => 'container', + '#states' => [ + 'visible' => [ + ':input[name="delete_associated_content"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['container']['media_items'] = [ + '#theme' => 'item_list', + '#type' => 'ul', + '#items' => $media_list, + '#attributes' => ['class' => ['islandora-media-items']], + '#wrapper_attributes' => ['class' => ['container']], + '#attached' => [ + 'library' => [ + 'islandora/islandora', + ], + ], + ]; + + $form['actions']['submit']['#submit'][] = 'islandora_object_delete_form_submit'; + return $form; + } + } + } + + return $form; +} + +/** + * Implements a submit handler for the delete form. + */ +function islandora_object_delete_form_submit($form, FormStateInterface $form_state) { + + $result = $form_state->getValues('delete_associated_content'); + $utils = \Drupal::service('islandora.utils'); + + if ($result['delete_associated_content'] == 1) { + + $node = $form_state->getFormObject()->getEntity(); + $medias = $utils->getMedia($node); + $media_list = []; + + $entity_field_manager = \Drupal::service('entity_field.manager'); + $current_user = \Drupal::currentUser(); + $logger = \Drupal::logger('logger.channel.islandora'); + $messenger = \Drupal::messenger(); + + $delete_media = []; + $media_translations = []; + $media_files = []; + $entity_protected_medias = []; + $inaccessible_entities = []; + + foreach ($medias as $id => $media) { + $lang = $media->language()->getId(); + $selected_langcodes[$lang] = $lang; + + if (!$media->access('delete', $current_user)) { + $inaccessible_entities[] = $media; + continue; + } + // Check for files. + $fields = $entity_field_manager->getFieldDefinitions('media', $media->bundle()); + foreach ($fields as $field) { + $type = $field->getType(); + if ($type == 'file' || $type == 'image') { + $target_id = $media->get($field->getName())->target_id; + $file = File::load($target_id); + if ($file) { + if (!$file->access('delete', $current_user)) { + $inaccessible_entities[] = $file; + continue; + } + $media_files[$id][$file->id()] = $file; + } + } + } + + foreach ($selected_langcodes as $langcode) { + // We're only working with media, which are translatable. + $entity = $media->getTranslation($langcode); + if ($entity->isDefaultTranslation()) { + $delete_media[$id] = $entity; + unset($media_translations[$id]); + } + elseif (!isset($delete_media[$id])) { + $media_translations[$id][] = $entity; + } + } + } + + if ($delete_media) { + foreach ($delete_media as $id => $media) { + try { + $media->delete(); + $media_list[] = $id; + $logger->notice('The media %label has been deleted.', [ + '%label' => $media->label(), + ]); + } + catch (Exception $e) { + $entity_protected_medias[] = $id; + } + } + } + + $delete_files = array_filter($media_files, function ($media) use ($entity_protected_medias) { + return !in_array($media, $entity_protected_medias); + }, ARRAY_FILTER_USE_KEY); + + if ($delete_files) { + foreach ($delete_files as $files_array) { + foreach ($files_array as $file) { + $file->delete(); + $logger->notice('The file %label has been deleted.', [ + '%label' => $file->label(), + ]); + } + } + } + + $delete_media_translations = array_filter($media_translations, function ($media) use ($entity_protected_medias) { + return !in_array($media, $entity_protected_medias); + }, ARRAY_FILTER_USE_KEY); + + if ($delete_media_translations) { + foreach ($delete_media_translations as $id => $translations) { + $media = $medias[$id]; + foreach ($translations as $translation) { + $media->removeTranslation($translation->language()->getId()); + } + $media->save(); + foreach ($translations as $translation) { + $logger->notice('The media %label @language translation has been deleted', [ + '%label' => $media->label(), + '@language' => $translation->language()->getName(), + ]); + } + } + } + + if ($inaccessible_entities) { + $messenger->addWarning("@count items have not been deleted because you do not have the necessary permissions.", [ + '@count' => count($inaccessible_entities), + ]); + } + + $build = [ + 'heading' => [ + '#type' => 'html_tag', + '#tag' => 'div', + '#value' => t("The repository item @node and @media", [ + '@node' => $node->getTitle(), + '@media' => \Drupal::translation()->formatPlural( + count($media_list), 'the media with the id @media has been deleted.', + 'the medias with the ids @media have been deleted.', + ['@media' => implode(", ", $media_list)], + ), + ]), + ], + ]; + + $message = \Drupal::service('renderer')->renderPlain($build); + $messenger->deleteByType('status'); + $messenger->addStatus($message); + } } /** @@ -515,7 +709,7 @@ function islandora_preprocess_views_view_table(&$variables) { // Check for a weight selector field. foreach ($variables['view']->field as $field_key => $field) { - if ($field->options['plugin_id'] == 'integer_weight_selector') { + if ($field->getPluginId() == 'integer_weight_selector') { // Check if the weight selector is on the first column. $is_first_column = array_search($field_key, array_keys($variables['view']->field)) > 0 ? FALSE : TRUE; diff --git a/islandora.post_update.php b/islandora.post_update.php new file mode 100644 index 00000000..0a29e56e --- /dev/null +++ b/islandora.post_update.php @@ -0,0 +1,16 @@ +<?php + +/** + * @file + * Post updates. + */ + +/** + * Set default value for delete_media_and_files field in settings. + */ +function islandora_post_update_delete_media_and_files() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('islandora.settings'); + $config->set('delete_media_and_files', TRUE); + $config->save(TRUE); +} diff --git a/modules/islandora_core_feature/config/install/filehash.settings.yml b/modules/islandora_core_feature/config/install/filehash.settings.yml index 7deecc69..3dcae297 100644 --- a/modules/islandora_core_feature/config/install/filehash.settings.yml +++ b/modules/islandora_core_feature/config/install/filehash.settings.yml @@ -1,5 +1,24 @@ algos: - sha1: sha1 + blake2b_128: '0' + blake2b_160: '0' + blake2b_224: '0' + blake2b_256: '0' + blake2b_384: '0' + blake2b_512: '0' md5: '0' + sha1: sha1 + sha224: '0' sha256: '0' + sha384: '0' + sha512_224: '0' + sha512_256: '0' + sha512: '0' + sha3_224: '0' + sha3_256: '0' + sha3_384: '0' + sha3_512: '0' dedupe: 0 +rehash: true +original: true +dedupe_original: false +mime_types: { } diff --git a/modules/islandora_core_feature/config/install/views.view.file_checksum.yml b/modules/islandora_core_feature/config/install/views.view.file_checksum.yml index b498ba66..2c819101 100644 --- a/modules/islandora_core_feature/config/install/views.view.file_checksum.yml +++ b/modules/islandora_core_feature/config/install/views.view.file_checksum.yml @@ -1,14 +1,15 @@ langcode: en status: true dependencies: - enforced: - module: - - islandora_core_feature module: - file - filehash - rest - serialization + - user + enforced: + module: + - islandora_core_feature id: file_checksum label: 'File Checksum' module: views @@ -16,71 +17,24 @@ description: 'Exposes a REST endpoint for getting the checksum of a File' tag: '' base_table: file_managed base_field: fid -core: 8.x display: default: - display_plugin: default id: default display_title: Master + display_plugin: default position: 0 display_options: - access: - type: none - options: { } - cache: - type: tag - options: { } - query: - type: views_query - options: - disable_sql_rewrite: false - distinct: false - replica: false - query_comment: '' - query_tags: { } - exposed_form: - type: basic - options: - submit_button: Apply - reset_button: false - reset_button_label: Reset - exposed_sorts_label: 'Sort by' - expose_sort_order: true - sort_asc_label: Asc - sort_desc_label: Desc - pager: - type: mini - options: - items_per_page: 10 - offset: 0 - id: 0 - total_pages: null - expose: - items_per_page: false - items_per_page_label: 'Items per page' - items_per_page_options: '5, 10, 25, 50' - items_per_page_options_all: false - items_per_page_options_all_label: '- All -' - offset: false - offset_label: Offset - tags: - previous: ‹‹ - next: ›› - style: - type: serializer - row: - type: 'entity:file' - options: - relationship: none - view_mode: default fields: sha1: id: sha1 - table: filehash + table: file_managed field: sha1 relationship: none group_type: group admin_label: '' + entity_type: file + entity_field: sha1 + plugin_id: field label: '' exclude: false alter: @@ -92,7 +46,7 @@ display: external: false replace_spaces: false path_case: none - trim_whitespace: false + trim_whitespace: true alt: '' rel: '' link_class: '' @@ -106,7 +60,7 @@ display: more_link: false more_link_text: '' more_link_path: '' - strip_tags: false + strip_tags: true trim: false preserve_tags: '' html: false @@ -122,13 +76,120 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true - plugin_id: standard - filters: { } - sorts: { } - header: { } - footer: { } + click_sort_column: value + type: filehash + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + original_sha1: + id: original_sha1 + table: file_managed + field: original_sha1 + relationship: none + group_type: group + admin_label: '' + entity_type: file + entity_field: original_sha1 + plugin_id: field + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: true + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: true + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: false + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: filehash + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + pager: + type: mini + options: + offset: 0 + items_per_page: 10 + total_pages: null + id: 0 + tags: + next: ›› + previous: ‹‹ + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'view checksums' + cache: + type: tag + options: { } empty: { } - relationships: { } + sorts: { } arguments: fid: id: fid @@ -137,6 +198,9 @@ display: relationship: none group_type: group admin_label: '' + entity_type: file + entity_field: fid + plugin_id: file_fid default_action: 'not found' exception: value: all @@ -151,8 +215,8 @@ display: summary_options: base_path: '' count: true - items_per_page: 25 override: false + items_per_page: 25 summary: sort_order: asc number_of_records: 0 @@ -164,31 +228,47 @@ display: validate_options: { } break_phrase: false not: false - entity_type: file - entity_field: fid - plugin_id: file_fid + filters: { } + style: + type: serializer + row: + type: 'entity:file' + options: + relationship: none + view_mode: default + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: { } + footer: { } display_extenders: { } cache_metadata: max-age: -1 contexts: + - 'languages:language_content' - 'languages:language_interface' - request_format - url - url.query_args + - user.permissions tags: { } rest_export_1: - display_plugin: rest_export id: rest_export_1 display_title: 'REST export' + display_plugin: rest_export position: 1 display_options: - display_extenders: { } - path: checksum/%file pager: type: some options: - items_per_page: 10 offset: 0 + items_per_page: 10 style: type: serializer options: @@ -198,6 +278,19 @@ display: type: data_field options: field_options: { } + display_extenders: + matomo: + enabled: false + keyword_gets: '' + keyword_behavior: first + keyword_concat_separator: ' ' + category_behavior: none + category_gets: '' + category_concat_separator: ' ' + category_fallback: '' + category_facets: { } + category_facets_concat_separator: ', ' + path: checksum/%file auth: - basic_auth - jwt_auth @@ -205,7 +298,10 @@ display: cache_metadata: max-age: -1 contexts: + - 'languages:language_content' - 'languages:language_interface' - request_format - url + - user.permissions tags: { } + diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php index cc4b5e94..c2a2fbc3 100644 --- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -205,11 +205,13 @@ class IIIFManifest extends StylePluginBase { continue; } - $ocrs = $entity->{$ocrField->definition['field_name']}; + if (!is_null($ocrField)) { + $ocrs = $entity->{$ocrField->definition['field_name']}; + $ocr = isset($ocrs[$i]) ? $ocrs[$i] : FALSE; + } // Create the IIIF URL for this file // Visiting $iiif_url will resolve to the info.json for the image. - $ocr = isset($ocrs[$i]) ? $ocrs[$i] : FALSE; $file_url = $image->entity->createFileUrl(FALSE); $mime_type = $image->entity->getMimeType(); $iiif_url = rtrim($iiif_address, '/') . '/' . urlencode($file_url); diff --git a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php index d6f99b58..a6e06774 100644 --- a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php +++ b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php @@ -6,7 +6,10 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\islandora\Plugin\Action\AbstractGenerateDerivativeMediaFile; /** - * Emits a Node for generating derivatives event. + * Emits a Media for generating derivatives event. + * + * Attaches the result as a file in an image field on the emitting + * Media ("multi-file media"). * * @Action( * id = "generate_image_derivative_file", @@ -24,7 +27,6 @@ class GenerateImageDerivativeFile extends AbstractGenerateDerivativeMediaFile { $config['path'] = '[date:custom:Y]-[date:custom:m]/[media:mid]-ImageService.jpg'; $config['mimetype'] = 'application/xml'; $config['queue'] = 'islandora-connector-houdini'; - $config['destination_media_type'] = 'file'; $config['scheme'] = $this->config->get('default_scheme'); return $config; } @@ -34,9 +36,30 @@ class GenerateImageDerivativeFile extends AbstractGenerateDerivativeMediaFile { */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { $form = parent::buildConfigurationForm($form, $form_state); - $form['mimetype']['#description'] = $this->t('Mimetype to convert to (e.g. application/xml, etc...)'); + $map = $this->entityFieldManager->getFieldMapByFieldType('image'); + $file_fields = $map['media']; + $file_options = array_combine(array_keys($file_fields), array_keys($file_fields)); + $file_options = array_merge(['' => ''], $file_options); + // @todo figure out how to write to thumbnail, which is not a real field. + // see https://github.com/Islandora/islandora/issues/891. + unset($file_options['thumbnail']); + + $form['destination_field_name'] = [ + '#required' => TRUE, + '#type' => 'select', + '#options' => $file_options, + '#title' => $this->t('Destination Image field'), + '#default_value' => $this->configuration['destination_field_name'], + '#description' => $this->t('This Action stores the derivative in an + Image field. If you are creating a TIFF or JP2, instead use + "Generate a Derivative File for Media Attachment". Selected target field + must be an additional field, not the media\'s main storage field. + Selected target field must be present on the media.'), + ]; + $form['mimetype']['#value'] = 'image/jpeg'; - $form['mimetype']['#type'] = 'hidden'; + $form['mimetype']['#description'] = 'Mimetype to convert to. Must be + compatible with the destination image field.'; return $form; } diff --git a/src/Controller/ManageMediaController.php b/src/Controller/ManageMediaController.php index bd670561..025d1d9d 100644 --- a/src/Controller/ManageMediaController.php +++ b/src/Controller/ManageMediaController.php @@ -6,6 +6,7 @@ use Drupal\islandora\IslandoraUtils; use Drupal\Core\Access\AccessResult; use Drupal\Core\Routing\RouteMatch; use Drupal\node\Entity\Node; +use Drupal\Core\Url; use Drupal\node\NodeInterface; /** @@ -25,7 +26,7 @@ class ManageMediaController extends ManageMembersController { public function addToNodePage(NodeInterface $node) { $field = IslandoraUtils::MEDIA_OF_FIELD; - return $this->generateTypeList( + $add_media_list = $this->generateTypeList( 'media', 'media_type', 'entity.media.add_form', @@ -33,6 +34,21 @@ class ManageMediaController extends ManageMembersController { $field, ['query' => ["edit[$field][widget][0][target_id]" => $node->id()]] ); + + $manage_link = Url::fromRoute('entity.media_type.collection')->toRenderArray(); + $manage_link['#title'] = $this->t('Manage media types'); + $manage_link['#type'] = 'link'; + $manage_link['#prefix'] = ' '; + $manage_link['#suffix'] = '.'; + + return [ + '#type' => 'markup', + '#markup' => $this->t("The following media types can be added because they have the <code>@field</code> field.", [ + '@field' => $field, + ]), + 'manage_link' => $manage_link, + 'add_media' => $add_media_list, + ]; } /** diff --git a/src/Controller/ManageMembersController.php b/src/Controller/ManageMembersController.php index 7f480fb3..9827ff35 100644 --- a/src/Controller/ManageMembersController.php +++ b/src/Controller/ManageMembersController.php @@ -7,6 +7,7 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\Entity\Controller\EntityController; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Link; +use Drupal\Core\Url; use Drupal\islandora\IslandoraUtils; use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -88,7 +89,8 @@ class ManageMembersController extends EntityController { */ public function addToNodePage(NodeInterface $node) { $field = IslandoraUtils::MEMBER_OF_FIELD; - return $this->generateTypeList( + + $add_node_list = $this->generateTypeList( 'node', 'node_type', 'node.add', @@ -96,6 +98,21 @@ class ManageMembersController extends EntityController { $field, ['query' => ["edit[$field][widget][0][target_id]" => $node->id()]] ); + + $manage_link = Url::fromRoute('entity.node_type.collection')->toRenderArray(); + $manage_link['#title'] = $this->t('Manage content types'); + $manage_link['#type'] = 'link'; + $manage_link['#prefix'] = ' '; + $manage_link['#suffix'] = '.'; + + return [ + '#type' => 'markup', + '#markup' => $this->t("The following content types can be added because they have the <code>@field</code> field.", [ + '@field' => $field, + ]), + 'manage_link' => $manage_link, + 'add_node' => $add_node_list, + ]; } /** diff --git a/src/Controller/MediaSourceController.php b/src/Controller/MediaSourceController.php index bab43fcf..3d0e7909 100644 --- a/src/Controller/MediaSourceController.php +++ b/src/Controller/MediaSourceController.php @@ -280,8 +280,7 @@ class MediaSourceController extends ControllerBase { */ 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')); + return AccessResult::allowedIf($media->access('update', $account)); } } diff --git a/src/EventGenerator/EmitEvent.php b/src/EventGenerator/EmitEvent.php index 683f3e8b..fd33fd99 100644 --- a/src/EventGenerator/EmitEvent.php +++ b/src/EventGenerator/EmitEvent.php @@ -14,6 +14,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\islandora\Event\StompHeaderEvent; use Drupal\islandora\Event\StompHeaderEventException; +use Drupal\islandora\Exception\IslandoraDerivativeException; use Stomp\Exception\StompException; use Stomp\StatefulStomp; use Stomp\Transport\Message; @@ -168,6 +169,10 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact $event->getHeaders()->all() ); } + catch (IslandoraDerivativeException $e) { + $this->logger->info($e->getMessage()); + return; + } catch (StompHeaderEventException $e) { $this->logger->error($e->getMessage()); $this->messenger->addError($e->getMessage()); diff --git a/src/Exception/IslandoraDerivativeException.php b/src/Exception/IslandoraDerivativeException.php new file mode 100644 index 00000000..7efe4773 --- /dev/null +++ b/src/Exception/IslandoraDerivativeException.php @@ -0,0 +1,11 @@ +<?php + +namespace Drupal\islandora\Exception; + +/** + * Islandora exceptions. + * + * @package islandora + */ +class IslandoraDerivativeException extends \RuntimeException { +} diff --git a/src/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php index 5049ef16..90e0b420 100644 --- a/src/Form/IslandoraSettingsForm.php +++ b/src/Form/IslandoraSettingsForm.php @@ -42,6 +42,7 @@ class IslandoraSettingsForm extends ConfigFormBase { 'year', ]; const GEMINI_PSEUDO_FIELD = 'field_gemini_uri'; + const NODE_DELETE_MEDIA_AND_FILES = 'delete_media_and_files'; /** * To list the available bundle types. @@ -201,6 +202,14 @@ class IslandoraSettingsForm extends ConfigFormBase { $fedora_url = NULL; } + $form[self::NODE_DELETE_MEDIA_AND_FILES] = [ + '#type' => 'checkbox', + '#title' => $this->t('Node Delete with Media and Files'), + '#description' => $this->t('Adds a checkbox in the "Delete" tab of islandora objects to delete media and files associated with the object.' + ), + '#default_value' => (bool) $config->get(self::NODE_DELETE_MEDIA_AND_FILES), + ]; + $form[self::FEDORA_URL] = [ '#type' => 'textfield', '#title' => $this->t('Fedora URL'), @@ -351,6 +360,7 @@ class IslandoraSettingsForm extends ConfigFormBase { ->set(self::UPLOAD_FORM_LOCATION, $form_state->getValue(self::UPLOAD_FORM_LOCATION)) ->set(self::UPLOAD_FORM_ALLOWED_MIMETYPES, $form_state->getValue(self::UPLOAD_FORM_ALLOWED_MIMETYPES)) ->set(self::GEMINI_PSEUDO, $new_pseudo_types) + ->set(self::NODE_DELETE_MEDIA_AND_FILES, $form_state->getValue(self::NODE_DELETE_MEDIA_AND_FILES)) ->save(); parent::submitForm($form, $form_state); diff --git a/src/MediaSource/MediaSourceService.php b/src/MediaSource/MediaSourceService.php index 4a10dafa..53378985 100644 --- a/src/MediaSource/MediaSourceService.php +++ b/src/MediaSource/MediaSourceService.php @@ -276,8 +276,8 @@ class MediaSourceService { 'uri' => $content_location, 'filename' => $this->fileSystem->basename($content_location), 'filemime' => $mimetype, - 'status' => FILE_STATUS_PERMANENT, ]); + $file->setPermanent(); // Validate file extension. $source_field_config = $this->entityTypeManager->getStorage('field_config')->load("media.$bundle.$source_field"); @@ -357,8 +357,8 @@ class MediaSourceService { 'uri' => $content_location, 'filename' => $this->fileSystem->basename($content_location), 'filemime' => $mimetype, - 'status' => FileInterface::STATUS_PERMANENT, ]); + $file->setPermanent(); // Validate file extension. $bundle = $media->bundle(); diff --git a/src/Plugin/Action/AbstractGenerateDerivative.php b/src/Plugin/Action/AbstractGenerateDerivative.php index b22201e1..b44db477 100644 --- a/src/Plugin/Action/AbstractGenerateDerivative.php +++ b/src/Plugin/Action/AbstractGenerateDerivative.php @@ -5,6 +5,7 @@ namespace Drupal\islandora\Plugin\Action; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; +use Drupal\islandora\Exception\IslandoraDerivativeException; /** * Emits a Node event. @@ -60,6 +61,13 @@ class AbstractGenerateDerivative extends AbstractGenerateDerivativeBase { throw new \RuntimeException("Could not locate taxonomy term with uri: " . $this->configuration['derivative_term_uri'], 500); } + // See if there is a destination media already set, and abort if it's the + // same as the source media. Dont cause an error, just don't continue. + $derivative_media = $this->utils->getMediaWithTerm($entity, $derivative_term); + if (!is_null($derivative_media) && $derivative_media->id() == $source_media->id()) { + throw new IslandoraDerivativeException("Halting derivative, as source and target media are the same. Derivative term: [" . $this->configuration['derivative_term_uri'] . "] Source term: [" . $this->configuration['source_term_uri'] . "] Node id: [" . $entity->id() . "].", 500); + } + $route_params = [ 'node' => $entity->id(), 'media_type' => $this->configuration['destination_media_type'], diff --git a/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php index be9eca80..f0974b0c 100644 --- a/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php +++ b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php @@ -7,7 +7,10 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; /** - * Emits a Node for generating derivatives event. + * Emits a Media for generating derivatives event. + * + * Attaches the result as a file in a file field on the emitting + * Media ("multi-file media"). * * @Action( * id = "generate_derivative_file", @@ -98,15 +101,23 @@ class AbstractGenerateDerivativeMediaFile extends AbstractGenerateDerivativeBase $image_options = array_combine(array_keys($image_fields), array_keys($image_fields)); $file_options = array_merge(['' => ''], $file_options, $image_options); + + // @todo figure out how to write to thumbnail, which is not a real field. + // see https://github.com/Islandora/islandora/issues/891. + unset($file_options['thumbnail']); + $form['event']['#disabled'] = 'disabled'; $form['destination_field_name'] = [ '#required' => TRUE, '#type' => 'select', '#options' => $file_options, - '#title' => $this->t('Destination File field Name'), + '#title' => $this->t('Destination File field'), '#default_value' => $this->configuration['destination_field_name'], - '#description' => $this->t('File field on Media Type to hold derivative. Cannot be the same as source'), + '#description' => $this->t('This Action stores a derivative file + in a File or Image field on a media. The destination field + must be an additional field, not the media\'s main storage field. + Selected destination field must be present on the media.'), ]; $form['args'] = [ diff --git a/src/Plugin/views/filter/NodeHasMediaUse.php b/src/Plugin/views/filter/NodeHasMediaUse.php index 0209505d..3c69bddd 100644 --- a/src/Plugin/views/filter/NodeHasMediaUse.php +++ b/src/Plugin/views/filter/NodeHasMediaUse.php @@ -18,10 +18,10 @@ class NodeHasMediaUse extends FilterPluginBase { * {@inheritdoc} */ protected function defineOptions() { - return [ - 'use_uri' => ['default' => NULL], - 'negated' => ['default' => FALSE], - ]; + $options = parent::defineOptions(); + $options['use_uri'] = ['default' => NULL]; + $options['negated'] = ['default' => FALSE]; + return $options; } /** diff --git a/tests/src/Functional/DeleteNodeWithMediaAndFile.php b/tests/src/Functional/DeleteNodeWithMediaAndFile.php new file mode 100644 index 00000000..40e469c5 --- /dev/null +++ b/tests/src/Functional/DeleteNodeWithMediaAndFile.php @@ -0,0 +1,104 @@ +<?php + +namespace Drupal\Tests\islandora\Functional; + +/** + * Tests the Delete Node with Media. + * + * @group islandora + */ +class DeleteNodeWithMediaAndFile extends IslandoraFunctionalTestBase { + + /** + * Tests delete Node and its assoicated media. + */ + public function testDeleteNodeWithMediaAndFile() { + $account = $this->drupalCreateUser([ + 'delete any media', + 'create media', + 'view media', + 'bypass node access', + 'access files overview', + 'administer site configuration', + ]); + $this->drupalLogin($account); + + $assert_session = $this->assertSession(); + + $testImageMediaType = $this->createMediaType('image', ['id' => 'test_image_media_type']); + $testImageMediaType->save(); + + $this->createEntityReferenceField('media', $testImageMediaType->id(), 'field_media_of', 'Media Of', 'node', 'default', [], 2); + + $node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => 'test_type', + 'title' => 'node', + ]); + $node->save(); + + // Make an image for the Media. + $file = $this->container->get('entity_type.manager')->getStorage('file')->create([ + 'uid' => $account->id(), + 'uri' => "public://test.jpeg", + 'filename' => "test.jpeg", + 'filemime' => "image/jpeg", + 'status' => FILE_STATUS_PERMANENT, + ]); + $file->save(); + + $this->drupalGet("node/1/delete"); + $assert_session->pageTextNotContains('Delete all associated medias and nodes'); + + // Make the media, and associate it with the image and node. + $media1 = $this->container->get('entity_type.manager')->getStorage('media')->create([ + 'bundle' => $testImageMediaType->id(), + 'name' => 'Media1', + 'field_media_image' => + [ + 'target_id' => $file->id(), + 'alt' => 'Some Alt', + 'title' => 'Some Title', + ], + 'field_media_of' => ['target_id' => $node->id()], + ]); + $media1->save(); + + $media2 = $this->container->get('entity_type.manager')->getStorage('media')->create([ + 'bundle' => $testImageMediaType->id(), + 'name' => 'Media2', + 'field_media_image' => + [ + 'target_id' => $file->id(), + 'alt' => 'Some Alt', + 'title' => 'Some Title', + ], + 'field_media_of' => ['target_id' => $node->id()], + ]); + $media2->save(); + + $this->drupalGet("admin/config/islandora/core"); + $assert_session->pageTextContains('Node Delete with Media and Files'); + \Drupal::configFactory()->getEditable('islandora.settings')->set('delete_media_and_files', TRUE)->save(); + + $delete = ['delete_associated_content' => TRUE]; + + $this->drupalGet("node/1/delete"); + $assert_session->pageTextContains('Media1'); + $assert_session->pageTextContains('Media2'); + $this->submitForm($delete, 'Delete'); + + $assert_session->pageTextContains($media1->id()); + $assert_session->pageTextContains($media2->id()); + + $this->drupalGet("media/1/delete"); + $assert_session->pageTextContains('Page not found'); + + $this->drupalGet("media/2/delete"); + $assert_session->pageTextContains('Page not found'); + + $this->drupalGet("/admin/content/files"); + $assert_session->pageTextNotContains('test.jpeg'); + + } + +}