diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index df4255ee..a9417abe 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -40,4 +40,4 @@ Any additional information that you think would be helpful when reviewing this PR. # Interested parties -Tag (@ mention) interested parties or, if unsure, @Islandora/8-x-committers +Tag (@ mention) interested parties or, if unsure, @Islandora/committers diff --git a/.github/workflows/build-2.x.yml b/.github/workflows/build-2.x.yml index 306278ba..8e4e19be 100644 --- a/.github/workflows/build-2.x.yml +++ b/.github/workflows/build-2.x.yml @@ -19,17 +19,23 @@ jobs: build: # The type of runner that the job will run on runs-on: ubuntu-latest + continue-on-error: ${{ matrix.allowed_failure }} strategy: + fail-fast: false matrix: - php-versions: ["7.3", "7.4"] + php-versions: ["8.1", "8.2"] + # test-suite functional-javascript will appear to pass but will skip tests; missing chromedriver. test-suite: ["kernel", "functional", "functional-javascript"] - drupal-version: ["8.9.11", "9.1.5"] + drupal-version: ["10.0.x", "10.1.x", "10.2.x-dev"] + mysql: ["8.0"] + allowed_failure: [false] - name: PHP ${{ matrix.php-versions }} drupal ${{ matrix.drupal-version }} test-suite ${{ matrix.test-suite }} + + name: PHP ${{ matrix.php-versions }} | drupal ${{ matrix.drupal-version }} | mysql ${{ matrix.mysql }} | test-suite ${{ matrix.test-suite }} services: mysql: - image: mysql:5.7 + image: mysql:${{ matrix.mysql }} env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: drupal @@ -44,14 +50,15 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: build_dir - name: Checkout islandora_ci - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: islandora/islandora_ci ref: github-actions @@ -66,6 +73,7 @@ jobs: - name: Setup Mysql client run: | sudo apt-get update + sudo apt-get remove -y mysql-client mysql-common sudo apt-get install -y mysql-client - name: Set environment variables @@ -76,7 +84,7 @@ jobs: echo "PHPUNIT_FILE=$GITHUB_WORKSPACE/build_dir/phpunit.xml" >> $GITHUB_ENV - name: Cache Composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/composer-cache key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} @@ -113,4 +121,3 @@ jobs: run: | cd $DRUPAL_DIR/web/core $DRUPAL_DIR/vendor/bin/phpunit --verbose --testsuite "${{ matrix.test-suite }}" - diff --git a/.github/workflows/gitlab-mirror.yml b/.github/workflows/gitlab-mirror.yml new file mode 100644 index 00000000..f59207e8 --- /dev/null +++ b/.github/workflows/gitlab-mirror.yml @@ -0,0 +1,26 @@ +name: Mirror and run GitLab CI + +on: + push: + branches: [2.x] + tags: '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Mirror + trigger CI + uses: SvanBoxel/gitlab-mirror-and-ci-action@master + with: + args: "https://git.drupalcode.org/project/islandora" + env: + FOLLOW_TAGS: "true" + FORCE_PUSH: "false" + GITLAB_HOSTNAME: "git.drupal.org" + GITLAB_USERNAME: "project_34868_bot" + GITLAB_PASSWORD: ${{ secrets.GITLAB_PASSWORD }} + GITLAB_PROJECT_ID: "34868" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index c085764d..546d2192 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ![Islandora](https://cloud.githubusercontent.com/assets/2371345/25624809/f95b0972-2f30-11e7-8992-a8f135402cdc.png) Islandora +# Islandora -[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg?style=flat-square)](https://php.net/) +[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.4-8892BF.svg?style=flat-square)](https://php.net/) [![Build Status](https://github.com/islandora/islandora/actions/workflows/build-2.x.yml/badge.svg)](https://github.com/Islandora/islandora/actions) [![Contribution Guidelines](http://img.shields.io/badge/CONTRIBUTING-Guidelines-blue.svg)](./CONTRIBUTING.md) [![LICENSE](https://img.shields.io/badge/license-GPLv2-blue.svg?style=flat-square)](./LICENSE) @@ -40,7 +40,6 @@ Installing via composer will download all required libraries and modules. Howev - [eva](http://drupal.org/project/eva) - [features](http://drupal.org/project/features) - [migrate_plus](http://drupal.org/project/migrate_plus) -- [migrate_tools](http://drupal.org/project/migrate_tools) - [migrate_source_csv](http://drupal.org/project/migrate_source_csv) - [flysystem](http://drupal.org/project/flysystem) @@ -49,6 +48,8 @@ It also requires the following PHP libraries: - [Crayfish Commons](https://packagist.org/packages/islandora/crayfish-commons) - [Stomp PHP](http://drupal.org/project/) +If you are using a Drush version less than 10.4 you will also need to install and enable [migrate_tools](http://drupal.org/project/migrate_tools) separately. + ## Installation For a full digital repository solution, see our [installation documentation](https://islandora.github.io/documentation/installation/component_overview/). @@ -90,27 +91,27 @@ Having problems or solved a problem? Check out the Islandora google groups for a Current maintainers: -* [Danny Lamb](https://github.com/dannylamb) +* [Islandora Technical Advisory Group](https://github.com/Islandora/islandora-community/wiki/Technical-Advisory-Group-%28TAG%29) ## Sponsors -* UPEI -* discoverygarden inc. -* LYRASIS -* McMaster University -* University of Limerick -* York University -* University of Manitoba -* Simon Fraser University -* PALS -* American Philosophical Society -* Common Media Inc. +* [American Philosophical Society](https://www.amphilsoc.org/) +* [Born-Digital, Inc.](https://www.born-digital.com/) +* [discoverygarden inc.](https://www.discoverygarden.ca/) +* [LYRASIS](https://www.lyrasis.org/) +* [McMaster University](https://www.mcmaster.ca/) +* [PALS](https://www.mnpals.org/) +* [University of Limerick](https://www.ul.ie/) +* [University of Manitoba](https://umanitoba.ca/) +* [UPEI](https://www.upei.ca/) +* [Simon Fraser University](https://www.sfu.ca/) +* [York University](https://www.yorku.ca/) ## Development -If you would like to contribute, please get involved by attending our weekly [Tech Call](https://github.com/Islandora/documentation/wiki). We love to hear from you! +If you would like to contribute, please get involved by attending our weekly [Tech Call](https://github.com/Islandora/islandora-community/wiki/Weekly-Open-Tech-Call). We love to hear from you! -If you would like to contribute code to the project, you need to be covered by an Islandora Foundation [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). Please see the [Contributors](http://islandora.ca/resources/contributors) pages on Islandora.ca for more information. +If you would like to contribute code to the project, you need to be covered by an Islandora Foundation [Contributor License Agreement](https://github.com/Islandora/islandora-community/wiki/Onboarding-Checklist#contributor-license-agreements) or [Corporate Contributor License Agreement](https://github.com/Islandora/islandora-community/wiki/Onboarding-Checklist#contributor-license-agreements). Please see the [Contributor License Agreements](https://github.com/Islandora/islandora-community/wiki/Contributor-License-Agreements) page on the islandora-community wiki for more information. We recommend using the [islandora-playbook](https://github.com/Islandora-Devops/islandora-playbook) to get started. diff --git a/composer.json b/composer.json index f787f94a..34107d37 100644 --- a/composer.json +++ b/composer.json @@ -14,34 +14,35 @@ } ], "require": { - "drupal/context": "^4.0@beta", - "drupal/search_api": "~1.8", - "islandora/jsonld": "^2", - "stomp-php/stomp-php": "4.*", - "drupal/jwt": "^1.0.0-beta5", - "drupal/filehash": "^1.1", - "drupal/prepopulate" : "^2.2", - "drupal/eva" : "^2.0", - "drupal/features" : "^3.7", - "drupal/migrate_plus" : "^5.1", - "drupal/migrate_tools" : "^5.0", + "drupal/context": "^4 || ^5@RC", + "drupal/ctools": "^3.8 || ^4", + "drupal/eva" : "^3.0", + "drupal/features" : "^3.13", + "drupal/file_replace": "^1.1", + "drupal/filehash": "^2", + "drupal/flysystem" : "^2.0@alpha", + "drupal/jwt": "^1.1 || ^2", + "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" - + "islandora/chullo": "^2.0", + "islandora/fedora-entity-mapper": "^1.0", + "islandora/jsonld": "^2 || ^3", + "stomp-php/stomp-php": "4.* || ^5" }, "require-dev": { "phpunit/phpunit": "^6", - "squizlabs/php_codesniffer": "2.7.1", + "squizlabs/php_codesniffer": "^2.7.1", "drupal/coder": "*", "sebastian/phpcpd": "*" }, "suggest": { - "drupal/transliterate_filenames": "Sanitizes filenames when they are uploaded so they don't break your repository." + "drupal/transliterate_filenames": "Sanitizes filenames when they are uploaded so they don't break your repository.", + "drupal/coi": "Some configuration fields work with Config Override Inspector." }, - "license": "GPL-2.0+", + "license": "GPL-2.0-or-later", "authors": [ { "name": "Islandora Foundation", diff --git a/config/install/islandora.settings.yml b/config/install/islandora.settings.yml index 8fb25fb8..1497c291 100644 --- a/config/install/islandora.settings.yml +++ b/config/install/islandora.settings.yml @@ -1,4 +1,4 @@ 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 de7a3e46..49de998b 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -14,15 +14,18 @@ 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' + redirect_after_media_save: + type: boolean + label: 'Redirect to node after media save.' upload_form_location: type: string label: 'Upload Form Location' upload_form_allowed_mimetypes: type: string label: 'Upload Form Allowed Extensions' - gemini_url: - type: uri - label: 'Url to Gemini microservice' gemini_pseudo_bundles: type: sequence label: 'List of node, media and taxonomy terms that should include the linked Fedora URI' @@ -87,6 +90,14 @@ condition.plugin.node_has_term: logic: type: string label: 'Logic (AND or OR)' + tids: + type: sequence + sequence: + type: mapping + mapping: + target_id: + type: integer + label: The target taxonomy term IDs condition.plugin.node_has_parent: type: condition.plugin @@ -160,12 +171,74 @@ condition.plugin.node_had_namespace: label: 'PID field' field.formatter.settings.islandora_image: - type: mapping - label: 'Image field display format settings' + type: field.formatter.settings.image + label: 'Islandora image field display format settings' + +condition.plugin.islandora_entity_bundle: + type: condition.plugin + mapping: + bundles: + type: sequence + sequence: + type: string + +condition.plugin.media_source_mimetype: + type: condition.plugin + mapping: + mimetype: + type: string + +reaction.plugin.alter_jsonld_type: + type: islandora.reaction_plugin_with_saved + mapping: + source_field: + type: string + +islandora.reaction_plugin_with_saved: + type: reaction.plugin mapping: - image_link: + saved: + type: boolean + label: Default config upstream; however, left undefined in the schema. + +reaction.plugin.islandora_map_uri_predicate: + type: islandora.reaction_plugin_with_saved + mapping: + drupal_uri_predicate: type: string - label: 'Link image to' - image_style: + +reaction.plugin.view_mode_alter: + type: islandora.reaction_plugin_with_saved + mapping: + mode: type: string - label: 'Image style' + label: The view mode to which to switch + +islandora.reaction.actions: + type: islandora.reaction_plugin_with_saved + mapping: + actions: + type: sequence + sequence: + type: string + +reaction.plugin.index: + type: islandora.reaction.actions + +reaction.plugin.delete: + type: islandora.reaction.actions + +reaction.plugin.derivative: + type: islandora.reaction.actions + +field.widget.settings.media_track: + type: field.widget.settings.file_generic + +field.field_settings.media_track: + type: field.field_settings.file + mapping: + languages: + type: string + +field.storage_settings.media_track: + type: field.storage_settings.file 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.info.yml b/islandora.info.yml index c66c1ba8..34e8118a 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -4,32 +4,31 @@ name: 'islandora' description: "Islandora Core" type: module package: Islandora -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 dependencies: + - context:context_ui + - ctools:ctools + - drupal:action + - drupal:basic_auth - drupal:block + - drupal:content_translation + - drupal:link + - drupal:media - drupal:node - - drupal:path - - drupal:text - drupal:options - - drupal:link - - drupal:jsonld - - drupal:search_api - - drupal:jwt + - drupal:path - drupal:rest - - drupal:filehash - - drupal:basic_auth - - drupal:context_ui - - drupal:action - - drupal:eva - drupal:taxonomy + - drupal:text - drupal:views_ui - - drupal:media - - drupal:prepopulate - - drupal:features_ui - - drupal:migrate_tools - - drupal:migrate_source_csv - - drupal:content_translation - - drupal:flysystem - - drupal:token - - drupal:file_replace + - eva:eva + - features:features_ui + - file_replace:file_replace + - filehash:filehash + - flysystem:flysystem + - jsonld:jsonld + - jwt:jwt + - migrate_source_csv:migrate_source_csv + - prepopulate:prepopulate + - search_api:search_api + - token:token diff --git a/islandora.install b/islandora.install index f9eb1225..01e5a467 100644 --- a/islandora.install +++ b/islandora.install @@ -5,6 +5,10 @@ * Install/update hook implementations. */ +use Drupal\Core\Extension\ExtensionNameLengthException; +use Drupal\Core\Extension\MissingDependencyException; +use Drupal\Core\Utility\UpdateException; + /** * Adds common namespaces to jsonld.settings. */ @@ -174,3 +178,51 @@ function update_jsonld_included_namespaces() { ->warning("Could not find required jsonld.settings to add default RDF namespaces."); } } + +/** + * Ensure that ctools is enabled. + */ +function islandora_update_8007() { + $module_handler = \Drupal::moduleHandler(); + if ($module_handler->moduleExists('ctools')) { + return t('The "@module_name" module is already enabled, no action necessary.', [ + '@module_name' => 'ctools', + ]); + } + + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $installer */ + $installer = \Drupal::service('module_installer'); + + try { + if ($installer->install(['ctools'], TRUE)) { + return t('The "@module_name" module was enabled successfully.', [ + '@module_name' => 'ctools', + ]); + } + } + catch (ExtensionNameLengthException | MissingDependencyException $e) { + throw new UpdateException('Failed; ensure that the ctools module is available in the Drupal installation.', 0, $e); + } + catch (\Exception $e) { + throw new UpdateException('Failed; encountered an exception while trying to enable ctools.', 0, $e); + } + + // Theoretically impossible to hit, as ModuleInstaller::install() only returns + // TRUE (or throws/propagates an exception), but... probably a good idea to + // have the here, just in case? + throw new UpdateException('Failed; hit the end of the update hook implementation, which is not expected.'); +} + +/** + * Set config to no redirect after media save. + */ +function islandora_update_8008() { + $config = \Drupal::configFactory()->getEditable('islandora.settings'); + if ($config) { + $config->set('redirect_after_media_save', FALSE); + $config->save(TRUE); + return t('A new configuration option, "Redirect after media save" is now available. + It has been turned off to preserve existing behaviour. To enable this setting visit + Configuration > Islandora > Core Settings.'); + } +} 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 738be5b4..d3dfa01d 100644 --- a/islandora.module +++ b/islandora.module @@ -26,6 +26,9 @@ use Drupal\media\MediaInterface; 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(). @@ -180,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; } @@ -251,8 +255,8 @@ function islandora_jsonld_alter_normalized_array(EntityInterface $entity, array $reaction->execute($entity, $normalized, $context); foreach ($context_manager->getActiveContexts() as $context_config) { try { - if ($context_config->getReaction($reaction->getPluginId())) { - $context['cacheability']->addCacheTags($context_config->getCacheTags()); + if ($context_config->getReaction($reaction->getPluginId()) && isset($context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY])) { + $context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($context_config); }; } catch (PluginNotFoundException $e) { @@ -328,19 +332,227 @@ function islandora_form_alter(&$form, FormStateInterface $form_state, $form_id) if ($node) { $form['name']['widget'][0]['value']['#default_value'] = $node->getTitle(); } + $form['actions']['submit']['#submit'][] = 'islandora_media_custom_form_submit'; } } + + $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; +} + +/** + * Redirect submit handler for media save. + */ +function islandora_media_custom_form_submit(&$form, FormStateInterface $form_state) { + // Check configuration to see whether a redirect is desired. + $redirect = \Drupal::config('islandora.settings')->get('redirect_after_media_save'); + if ($redirect) { + $params = \Drupal::request()->query->all(); + if (!empty($params)) { + $target_id = $params['edit']['field_media_of']['widget'][0]['target_id']; + $url = Url::fromRoute('view.media_of.page_1', ['node' => $target_id]); + $form_state->setRedirectUrl($url); + } + } +} + +/** + * 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); + } } /** - * Implements hook_field_widget_WIDGET_TYPE_form_alter(). + * Implements hook_field_widget_single_element_WIDGET_TYPE_form_alter(). */ -function islandora_field_widget_image_image_form_alter(&$element, $form_state, $context) { +function islandora_field_widget_single_element_image_image_form_alter(&$element, $form_state, $context) { $element['#process'][] = 'islandora_add_default_image_alt_text'; } /** - * Callback for hook_field_widget_WIDGET_TYPE_form_alter(). + * Callback for hook_field_widget_single_element_WIDGET_TYPE_form_alter(). */ function islandora_add_default_image_alt_text($element, $form_state, $form) { if ($element['alt']['#access']) { @@ -421,8 +633,8 @@ function islandora_entity_extra_field_info() { if (!empty($pseudo_bundles)) { foreach ($pseudo_bundles as $key) { - list($bundle, $content_entity) = explode(":", $key); - $extra_field[$content_entity][$bundle]['display']['field_gemini_uri'] = [ + [$bundle, $content_entity] = explode(":", $key); + $extra_field[$content_entity][$bundle]['display'][IslandoraSettingsForm::GEMINI_PSEUDO_FIELD] = [ 'label' => t('Fedora URI'), 'description' => t('The URI to the persistent'), 'weight' => 100, @@ -450,6 +662,17 @@ function islandora_entity_view(array &$build, EntityInterface $entity, EntityVie // Check if the source file is in Fedora or not. $media_source_service = \Drupal::service('islandora.media_source_service'); $source_file = $media_source_service->getSourceFile($entity); + if (!$source_file) { + \Drupal::logger('islandora')->error( + \Drupal::service('string_translation')->translate( + "Can't get source file for @label (@id)", [ + '@label' => $entity->label(), + "@id" => $entity->id(), + ] + ) + ); + return; + } $uri = $source_file->getFileUri(); $scheme = \Drupal::service('stream_wrapper_manager')->getScheme($uri); $flysystem_config = Settings::get('flysystem'); @@ -503,7 +726,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 @@ +getEditable('islandora.settings'); + $config->set('delete_media_and_files', TRUE); + $config->save(TRUE); +} diff --git a/islandora.routing.yml b/islandora.routing.yml index 6fea02e9..86d13482 100644 --- a/islandora.routing.yml +++ b/islandora.routing.yml @@ -37,15 +37,15 @@ islandora.add_member_to_node_page: _entity_create_any_access: 'node' islandora.upload_children: - path: '/node/{node}/members/upload' + path: '/node/{node}/members/upload/{step}' defaults: - _form: '\Drupal\islandora\Form\AddChildrenForm' + _wizard: '\Drupal\islandora\Form\AddChildrenWizard\ChildForm' _title: 'Upload Children' + step: 'type_selection' options: _admin_route: 'TRUE' requirements: - _access: 'TRUE' - #_permssion: 'create node,create media' + _custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::childAccess' islandora.add_media_to_node_page: path: '/node/{node}/media/add' @@ -59,14 +59,15 @@ islandora.add_media_to_node_page: _entity_create_any_access: 'media' islandora.upload_media: - path: '/node/{node}/media/upload' + path: '/node/{node}/media/upload/{step}' defaults: - _form: '\Drupal\islandora\Form\AddMediaForm' + _wizard: '\Drupal\islandora\Form\AddChildrenWizard\MediaForm' _title: 'Add media' + step: 'type_selection' options: _admin_route: 'TRUE' requirements: - _custom_access: '\Drupal\islandora\Form\AddMediaForm::access' + _custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::mediaAccess' islandora.media_source_update: path: '/media/{media}/source' diff --git a/islandora.services.yml b/islandora.services.yml index 4b3a9d16..74725d80 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -31,6 +31,9 @@ services: logger.channel.islandora: parent: logger.channel_base arguments: ['islandora'] + logger.channel.fedora_flysystem: + parent: logger.channel_base + arguments: ['fedora_flysystem'] islandora.media_route_context_provider: class: Drupal\islandora\ContextProvider\MediaRouteContextProvider arguments: ['@current_route_match'] @@ -53,9 +56,25 @@ services: class: Drupal\islandora\IslandoraUtils arguments: ['@entity_type.manager', '@entity_field.manager', '@context.manager', '@flysystem_factory', '@language_manager'] islandora.entity_mapper: - class: Islandora\Crayfish\Commons\EntityMapper\EntityMapper + class: Islandora\EntityMapper\EntityMapper islandora.stomp.auth_header_listener: class: Drupal\islandora\EventSubscriber\StompHeaderEventSubscriber arguments: ['@jwt.authentication.jwt'] tags: - { name: event_subscriber } + islandora.upload_children.batch_processor: + class: Drupal\islandora\Form\AddChildrenWizard\ChildBatchProcessor + arguments: + - '@entity_type.manager' + - '@database' + - '@current_user' + - '@messenger' + - '@date.formatter' + islandora.upload_media.batch_processor: + class: Drupal\islandora\Form\AddChildrenWizard\MediaBatchProcessor + arguments: + - '@entity_type.manager' + - '@database' + - '@current_user' + - '@messenger' + - '@date.formatter' diff --git a/islandora.tokens.inc b/islandora.tokens.inc index 8d1c4b6b..f528dee3 100644 --- a/islandora.tokens.inc +++ b/islandora.tokens.inc @@ -19,6 +19,18 @@ function islandora_token_info() { 'name' => t('Islandora Tokens'), 'description' => t('Tokens for Islandora objects.'), ]; + $node['media-original-file:filename'] = [ + 'name' => t('Media: Original File filename without extension.'), + 'description' => t('File name without extension of original uploaded file associated with Islandora Object via Media.'), + ]; + $node['media-original-file:basename'] = [ + 'name' => t('Media: Original File filename with extension.'), + 'description' => t('File name with extension of original uploaded file associated with Islandora Object via Media.'), + ]; + $node['media-original-file:extension'] = [ + 'name' => t('Media: Original File extension.'), + 'description' => t('File extension of original uploaded file associated with Islandora Object via Media.'), + ]; $node['media-thumbnail-image:url'] = [ 'name' => t('Media: Thumbnail Image URL.'), 'description' => t('URL of Thumbnail Image associated with Islandora Object via Media.'), @@ -70,6 +82,24 @@ function islandora_tokens($type, $tokens, array $data, array $options, Bubbleabl $islandoraUtils = \Drupal::service('islandora.utils'); foreach ($tokens as $name => $original) { switch ($name) { + case 'media-original-file:basename': + case 'media-original-file:filename': + case 'media-original-file:extension': + $term = $islandoraUtils->getTermForUri('http://pcdm.org/use#OriginalFile'); + $media = $islandoraUtils->getMediaWithTerm($data['node'], $term); + // Is there media? + if ($media) { + $file = \Drupal::service('islandora.media_source_service')->getSourceFile($media); + if (!empty($file)) { + $path_info = pathinfo($file->createFileUrl()); + $key = explode(':', $name)[1]; + if (array_key_exists($key, $path_info)) { + $replacements[$original] = $path_info[$key]; + } + } + } + break; + case 'media-thumbnail-image:url': case 'media_thumbnail_image:url': $term = $islandoraUtils->getTermForUri('http://pcdm.org/use#ThumbnailImage'); @@ -79,7 +109,7 @@ function islandora_tokens($type, $tokens, array $data, array $options, Bubbleabl if ($media) { $file = \Drupal::service('islandora.media_source_service')->getSourceFile($media); if (!empty($file)) { - $url = $file->url(); + $url = $file->createFileUrl(); $replacements[$original] = $url; } } @@ -104,7 +134,7 @@ function islandora_tokens($type, $tokens, array $data, array $options, Bubbleabl break; case 'pdf_url': - $replacements[$original] = ' ' . islandora_url_to_service_file_media_by_mimetype($data['node'], 'application/pdf'); + $replacements[$original] = islandora_url_to_service_file_media_by_mimetype($data['node'], 'application/pdf'); break; } } @@ -158,4 +188,5 @@ function islandora_url_to_service_file_media_by_mimetype($node, $mime_type) { } } } + return ''; } diff --git a/islandora.views.inc b/islandora.views.inc index cd826d08..f249c633 100644 --- a/islandora.views.inc +++ b/islandora.views.inc @@ -9,17 +9,46 @@ * Implements hook_views_data_alter(). */ function islandora_views_data_alter(&$data) { - // For now only support Nodes. - $fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('node'); - foreach ($fields as $field => $field_storage_definition) { - if ($field_storage_definition->getType() == 'integer' && strpos($field, "field_") === 0) { - $data['node__' . $field][$field . '_value']['field'] = $data['node__' . $field][$field]['field']; - $data['node__' . $field][$field]['title'] = t('Integer Weight Selector (@field)', [ - '@field' => $field, - ]); - $data['node__' . $field][$field]['help'] = t('Provides a drag-n-drop reordering of integer-based weight fields.'); - $data['node__' . $field][$field]['title short'] = t('Integer weight selector'); - $data['node__' . $field][$field]['field']['id'] = 'integer_weight_selector'; + // For now only support Nodes and Media. + foreach (['node', 'media'] as $entity_type) { + $fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type); + foreach ($fields as $field => $field_storage_definition) { + if ($field_storage_definition->getType() == 'integer' && strpos($field, "field_") === 0) { + $prefixed_field = $entity_type . '__' . $field; + if (isset($data[$prefixed_field])) { + $data[$prefixed_field][$field . '_value']['field'] = $data[$prefixed_field][$field]['field']; + $data[$prefixed_field][$field]['title'] = t('Integer Weight Selector (@field)', [ + '@field' => $field, + ]); + $data[$prefixed_field][$field]['help'] = t('Provides a drag-n-drop reordering of integer-based weight fields.'); + $data[$prefixed_field][$field]['title short'] = t('Integer weight selector'); + $data[$prefixed_field][$field]['field']['id'] = 'integer_weight_selector'; + } + } } } + + // Add Has Media filter. + $data['node_field_data']['islandora_has_media'] = [ + 'title' => t('Node has Media Use'), + 'group' => t('Content'), + 'filter' => [ + 'title' => t('Node has media use filter'), + 'help' => t('Provides a custom filter for nodes that do or do not have media with a given use.'), + 'field' => 'nid', + 'id' => 'islandora_node_has_media_use', + ], + ]; + + // Add Is Islandora filter. + $data['node_field_data']['islandora_node_is_islandora'] = [ + 'title' => t('Node is Islandora'), + 'group' => t('Content'), + 'filter' => [ + 'title' => t('Node is Islandora'), + 'help' => t('Node has a content type that possesses the mandatory Islandora fields.'), + 'field' => 'nid', + 'id' => 'islandora_node_is_islandora', + ], + ]; } diff --git a/modules/islandora_advanced_search/islandora_advanced_search.info.yml b/modules/islandora_advanced_search/islandora_advanced_search.info.yml index 524e9dc7..a5ad90ef 100644 --- a/modules/islandora_advanced_search/islandora_advanced_search.info.yml +++ b/modules/islandora_advanced_search/islandora_advanced_search.info.yml @@ -4,8 +4,10 @@ name: 'Islandora Advanced Search' description: "Creates an Advanced Search block and other enhancements to search." type: module package: Islandora -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 dependencies: - drupal:facets - drupal:facets_summary - drupal:search_api_solr +lifecycle: deprecated +lifecycle_link: https://groups.google.com/g/islandora/c/SEOAWJrfE_M diff --git a/modules/islandora_advanced_search/islandora_advanced_search.module b/modules/islandora_advanced_search/islandora_advanced_search.module index cbf52667..f08dd346 100644 --- a/modules/islandora_advanced_search/islandora_advanced_search.module +++ b/modules/islandora_advanced_search/islandora_advanced_search.module @@ -13,51 +13,13 @@ */ use Drupal\block\Entity\Block; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Form\FormStateInterface; use Drupal\islandora_advanced_search\AdvancedSearchQuery; -use Drupal\islandora_advanced_search\Form\SettingsForm; use Drupal\islandora_advanced_search\Utilities; use Drupal\search_api\Query\QueryInterface as DrupalQueryInterface; use Drupal\views\ViewExecutable; use Solarium\Core\Query\QueryInterface as SolariumQueryInterface; -/** - * Implements hook_theme(). - */ -function islandora_advanced_search_theme() { - return [ - 'facets_item_list__include_exclude_links' => [ - 'template' => 'facets/facets-item-list--include-exclude-links', - 'base hook' => 'facets_item_list', - ], - 'facets_result_item__include_exclude_links' => [ - 'template' => 'facets/facets-result-item--include-exclude-links', - 'base hook' => 'facets_result_item', - ], - 'facets_result_item__summary' => [ - 'template' => 'facets/facets-result-item--summary', - 'base hook' => 'facets_result_item', - ], - ]; -} - -/** - * Implements hook_library_info_alter(). - */ -function islandora_advanced_search_library_info_alter(&$libraries, $extension) { - if ($extension == 'facets') { - // Override facets module javascript with customizations. - $path = '/' . drupal_get_path('module', 'islandora_advanced_search') . '/js/facets'; - $libraries['soft-limit']['js'] = [ - "$path/soft-limit.js" => [], - ]; - $libraries['drupal.facets.views-ajax']['js'] = [ - "$path/facets-views-ajax.js" => [], - ]; - } -} - /** * Implements hook_search_api_solr_converted_query_alter(). */ @@ -99,20 +61,6 @@ function islandora_advanced_search_form_block_form_alter(&$form, FormStateInterf $form['visibility'][$condition_id] = $condition_form; } -/** - * Implements hook_preprocess_block__facets_summary(). - */ -function islandora_advanced_search_preprocess_block__facets_summary(&$variables) { - // Copy data-attributes to the content as the javascript expects - // there to be no elements between the data declaration and the - // content of the block. - foreach ($variables['attributes'] as $key => $value) { - if (substr($key, 0, 4) === "data") { - $variables['content_attributes'][$key] = $value; - } - } -} - /** * Implements hook_preprocess_preprocess_views_view(). */ @@ -139,53 +87,3 @@ function islandora_advanced_search_views_pre_view(ViewExecutable $view, $display $advanced_search_query = new AdvancedSearchQuery(); $advanced_search_query->alterView(\Drupal::request(), $view, $display_id); } - -/** - * Implements hook_preprocess_facets_summary_item_list(). - */ -function islandora_advanced_search_preprocess_facets_summary_item_list(&$variables) { - foreach ($variables['items'] as &$item) { - $item['attributes']['class'][] = 'facet-summary-item'; - } -} - -/** - * Implements hook_preprocess_facets_item_list(). - */ -function islandora_advanced_search_preprocess_facets_item_list(&$variables) { - $widget = $variables['facet']->getWidget(); - $soft_limit = $widget['config']['soft_limit']; - // Break into two groups less / more which can display be toggled as a single - // element change rather than showing / hiding all
  • elements individually. - // As its slow and causes the page to snap when loading. - $variables['less'] = array_slice($variables['items'], 0, $soft_limit); - $variables['more'] = array_slice($variables['items'], $soft_limit); - $variables['show_more_label'] = $widget['config']['soft_limit_settings']['show_more_label']; -} - -/** - * Implements hook_preprocess_facets_result_item(). - */ -function islandora_advanced_search_preprocess_facets_result_item(&$variables) { - $settings = \Drupal::config(SettingsForm::CONFIG_NAME); - $length = $settings->get(SettingsForm::FACET_TRUNCATE); - if (is_numeric($length)) { - // Limit the length of facets display to at most 32 characters. - if (is_string($variables['value'])) { - $variables['value'] = Unicode::truncate( - $variables['value'], - $length, - TRUE, - TRUE - ); - } - elseif (is_string($variables['value']['text']['#title'])) { - $variables['value']['text']['#title'] = Unicode::truncate( - $variables['value']['text']['#title'], - $length, - TRUE, - TRUE - ); - } - } -} diff --git a/modules/islandora_advanced_search/js/facets/facets-views-ajax.js b/modules/islandora_advanced_search/js/facets/facets-views-ajax.js deleted file mode 100644 index 0b04ea33..00000000 --- a/modules/islandora_advanced_search/js/facets/facets-views-ajax.js +++ /dev/null @@ -1,147 +0,0 @@ -//# sourceURL=modules/contrib/islandora/modules/islandora_advanced_search/js/facets/facets-view.ajax.js -/** - * @file - * Overrides the facets-view-ajax.js behavior from the 'facets' module. - */ -(function ($, Drupal) { - "use strict"; - - // Generate events on push state. - (function (history) { - var pushState = history.pushState; - history.pushState = function (state, title, url) { - var ret = pushState.apply(this, arguments); - var event = new Event("pushstate"); - window.dispatchEvent(event); - return ret; - }; - })(window.history); - - function reload(url) { - // Update View. - if (drupalSettings && drupalSettings.views && drupalSettings.views.ajaxViews) { - var view_path = drupalSettings.views.ajax_path; - $.each(drupalSettings.views.ajaxViews, function (views_dom_id) { - var views_parameters = Drupal.Views.parseQueryString(url); - var views_arguments = Drupal.Views.parseViewArgs(url, "search"); - var views_settings = $.extend( - {}, - Drupal.views.instances[views_dom_id].settings, - views_arguments, - views_parameters - ); - var views_ajax_settings = - Drupal.views.instances[views_dom_id].element_settings; - views_ajax_settings.submit = views_settings; - views_ajax_settings.url = - view_path + "?" + $.param(Drupal.Views.parseQueryString(url)); - Drupal.ajax(views_ajax_settings).execute(); - }); - } - - // Replace filter, pager, summary, and facet blocks. - var blocks = {}; - $( - ".block[class*='block-plugin-id--islandora-advanced-search-result-pager'], .block[class*='block-plugin-id--views-exposed-filter-block'], .block[class*='block-plugin-id--facet']" - ).each(function () { - var id = $(this).attr("id"); - var block_id = id - .slice("block-".length, id.length) - .replace(/--.*$/g, "") - .replace(/-/g, "_"); - blocks[block_id] = "#" + id; - }); - Drupal.ajax({ - url: Drupal.url("islandora-advanced-search-ajax-blocks"), - submit: { - link: url, - blocks: blocks, - }, - }).execute(); - } - - // On location change reload all the blocks / ajax view. - window.addEventListener("pushstate", function (e) { - reload(window.location.href); - }); - - window.addEventListener("popstate", function (e) { - if (e.state != null) { - reload(window.location.href); - } - }); - - /** - * Push state on form/pager/facet change. - */ - Drupal.behaviors.islandoraAdvancedSearchViewsAjax = { - attach: function (context, settings) { - window.historyInitiated = true; - - // Remove existing behavior from form. - if (settings && settings.views && settings.views.ajaxViews) { - $.each(settings.views.ajaxViews, function (index, settings) { - var exposed_form = $( - "form#views-exposed-form-" + - settings.view_name.replace(/_/g, "-") + - "-" + - settings.view_display_id.replace(/_/g, "-") - ); - exposed_form - .once() - .find("input[type=submit], input[type=image]") - .not("[data-drupal-selector=edit-reset]") - .each(function (index) { - $(this).unbind("click"); - $(this).click(function (e) { - // Let ctrl/cmd click open in a new window. - if (e.shiftKey || e.ctrlKey || e.metaKey) { - return; - } - e.preventDefault(); - e.stopPropagation(); - var href = window.location.href; - var params = Drupal.Views.parseQueryString(href); - // Remove the page if set as submitting the form should always take - // the user to the first page (facets do the same). - delete params.page; - // Include values from the form in the URL. - $.each(exposed_form.serializeArray(), function () { - params[this.name] = this.value; - }); - href = href.split("?")[0] + "?" + $.param(params); - window.history.pushState(null, document.title, href); - }); - }); - }); - } - - // Attach behavior to pager, summary, facet links. - $("[data-drupal-pager-id], [data-drupal-facets-summary-id], [data-drupal-facet-id]") - .once() - .find("a:not(.facets-soft-limit-link)") - .click(function (e) { - // Let ctrl/cmd click open in a new window. - if (e.shiftKey || e.ctrlKey || e.metaKey) { - return; - } - e.preventDefault(); - window.history.pushState(null, document.title, $(this).attr("href")); - }); - - // Trigger on sort change. - $('[data-drupal-pager-id] select[name="order"]') - .once() - .change(function () { - var href = window.location.href; - var params = Drupal.Views.parseQueryString(href); - var selection = $(this).val(); - var option = $('option[value="' + selection + '"]'); - params.sort_order = option.data("sort_order"); - params.sort_by = option.data("sort_by"); - href = href.split("?")[0] + "?" + $.param(params); - window.history.pushState(null, document.title, href); - }); - }, - }; -})(jQuery, Drupal); diff --git a/modules/islandora_advanced_search/js/facets/soft-limit.js b/modules/islandora_advanced_search/js/facets/soft-limit.js deleted file mode 100644 index a81a267c..00000000 --- a/modules/islandora_advanced_search/js/facets/soft-limit.js +++ /dev/null @@ -1,70 +0,0 @@ -//# sourceURL=modules/contrib/islandora/modules/islandora_advanced_search/js/facets/soft-limit.js -/** - * @file - * Overrides the soft-limit.js behavior from the 'facets' module. - * As when having many facets the original version causes the page to slow down and snap to hidden when rendering. - */ -(function ($) { - - 'use strict'; - - Drupal.behaviors.facetSoftLimit = { - attach: function (context, settings) { - if (settings.facets.softLimit !== 'undefined') { - $.each(settings.facets.softLimit, function (facet, limit) { - Drupal.facets.applySoftLimit(facet, limit, settings); - }); - } - } - }; - - Drupal.facets = Drupal.facets || {}; - - /** - * Applies the soft limit UI feature to a specific facets list. - * - * @param {string} facet - * The facet id. - * @param {string} limit - * The maximum amount of items to show. - * @param {object} settings - * Settings. - */ - Drupal.facets.applySoftLimit = function (facet, limit, settings) { - var zero_based_limit = (limit - 1); - var facet_id = facet; - var facetsList = $('ul[data-drupal-facet-id="' + facet_id + '"]'); - - // In case of multiple instances of a facet, we need to key them. - if (facetsList.length > 1) { - facetsList.each(function (key, $value) { - $(this).attr('data-drupal-facet-id', facet_id + '-' + key); - }); - } - - // Add "Show more" / "Show less" links. - facetsList.filter(function () { - return $(this).next('ul').length == 1; // Has expanding list. - }).each(function () { - var facet = $(this); - var expand = facet.next('ul'); - var link = expand.next('a'); - var showLessLabel = settings.facets.softLimitSettings[facet_id].showLessLabel; - var showMoreLabel = settings.facets.softLimitSettings[facet_id].showMoreLabel; - link.text(showMoreLabel) - .once() - .on('click', function () { - if (!expand.is(":visible")) { - expand.slideDown(); - $(this).addClass('open').text(showLessLabel); - } - else { - expand.slideUp(); - $(this).removeClass('open').text(showMoreLabel); - } - return false; - }) - }); - }; - -})(jQuery); diff --git a/modules/islandora_advanced_search/src/Form/AdvancedSearchForm.php b/modules/islandora_advanced_search/src/Form/AdvancedSearchForm.php index c00ce566..09dd1646 100644 --- a/modules/islandora_advanced_search/src/Form/AdvancedSearchForm.php +++ b/modules/islandora_advanced_search/src/Form/AdvancedSearchForm.php @@ -71,7 +71,7 @@ class AdvancedSearchForm extends FormBase { */ public static function create(ContainerInterface $container) { return new static( - $container->get('request_stack')->getMasterRequest(), + $container->get('request_stack')->getMainRequest(), $container->get('current_route_match') ); } diff --git a/modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlock.php b/modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlock.php index c5f5adb8..2aa77f12 100644 --- a/modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlock.php +++ b/modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlock.php @@ -108,7 +108,7 @@ class AdvancedSearchBlock extends BlockBase implements ContainerFactoryPluginInt $plugin_definition, $container->get('plugin.manager.search_api.display'), $container->get('form_builder'), - $container->get('request_stack')->getMasterRequest() + $container->get('request_stack')->getMainRequest() ); } diff --git a/modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlock.php b/modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlock.php index f2e4a170..e1e477d8 100644 --- a/modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlock.php +++ b/modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlock.php @@ -58,7 +58,7 @@ class SearchResultsPagerBlock extends BlockBase implements ContainerFactoryPlugi $configuration, $plugin_id, $plugin_definition, - $container->get('request_stack')->getMasterRequest() + $container->get('request_stack')->getMainRequest() ); } diff --git a/modules/islandora_audio/config/schema/islandora_audio.schema.yml b/modules/islandora_audio/config/schema/islandora_audio.schema.yml index 5f09740a..82b08088 100644 --- a/modules/islandora_audio/config/schema/islandora_audio.schema.yml +++ b/modules/islandora_audio/config/schema/islandora_audio.schema.yml @@ -29,3 +29,6 @@ action.configuration.generate_audio_derivative: path: type: text label: 'File path with extension' + +field.formatter.settings.islandora_file_audio: + type: field.formatter.settings.file_audio diff --git a/modules/islandora_audio/islandora_audio.info.yml b/modules/islandora_audio/islandora_audio.info.yml index 998590f5..5e6beb5a 100644 --- a/modules/islandora_audio/islandora_audio.info.yml +++ b/modules/islandora_audio/islandora_audio.info.yml @@ -2,7 +2,6 @@ name: 'Islandora Audio' description: 'Islandora audio derivative actions' type: module package: Islandora -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 dependencies: - drupal:islandora diff --git a/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php b/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php index fc1c6188..6b85cd1b 100644 --- a/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php +++ b/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php @@ -66,9 +66,10 @@ class GenerateAudioDerivativeTest extends GenerateDerivativeTestBase { 'name[0][value]' => 'Test Media', 'files[field_media_file_0]' => __DIR__ . '/../../fixtures/test_file.txt', 'field_media_of[0][target_id]' => 'Test Node', - 'field_tags[0][target_id]' => 'Preservation Master', + 'field_media_use[0][target_id]' => $this->preservationMasterTerm->label(), ]; - $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, $this->t('Save')); + $this->drupalGet('media/add/' . $this->testMediaType->id()); + $this->submitForm($values, $this->t('Save')); $expected = [ 'source_uri' => 'test_file.txt', diff --git a/modules/islandora_breadcrumbs/config/install/islandora_breadcrumbs.breadcrumbs.yml b/modules/islandora_breadcrumbs/config/install/islandora_breadcrumbs.breadcrumbs.yml index ad166b50..aabb5891 100644 --- a/modules/islandora_breadcrumbs/config/install/islandora_breadcrumbs.breadcrumbs.yml +++ b/modules/islandora_breadcrumbs/config/install/islandora_breadcrumbs.breadcrumbs.yml @@ -1,9 +1,4 @@ maxDepth: -1 includeSelf: FALSE -referenceField: field_member_of -dependencies: - module: - - islandora - enforced: - module: - - islandora_breadcrumbs +referenceFields: + - field_member_of diff --git a/modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml b/modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml index 6bc44096..4175cbf5 100644 --- a/modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml +++ b/modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml @@ -7,6 +7,9 @@ islandora_breadcrumbs.breadcrumbs: includeSelf: type: boolean label: 'Include Self' - referenceField: - type: string - label: 'Reference Field' + referenceFields: + type: sequence + label: 'Reference Fields' + sequence: + type: string + label: 'Reference Field' diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml index 56a10bc1..661ec70a 100644 --- a/modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml @@ -1,8 +1,7 @@ name: 'Islandora Breadcrumbs' type: module description: 'Builds breadcrumbs based on field_member_of relationships.' -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 package: Islandora dependencies: - - drupal:islandora + - islandora:islandora diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.install b/modules/islandora_breadcrumbs/islandora_breadcrumbs.install new file mode 100644 index 00000000..2ca9ada6 --- /dev/null +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.install @@ -0,0 +1,18 @@ +getEditable('islandora_breadcrumbs.breadcrumbs'); + $config->set('referenceFields', [$config->get('referenceField')]); + $config->clear('referenceField'); + $config->save(); + return "Updated referenceFields config."; +} diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.links.menu.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.links.menu.yml new file mode 100644 index 00000000..dcf99534 --- /dev/null +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.links.menu.yml @@ -0,0 +1,5 @@ +system.islandora_breadcrumbs_settings: + title: 'Breadcrumbs Settings' + parent: system.admin_config_islandora + route_name: system.islandora_breadcrumbs_settings + description: 'Configure Islandora breadcrumb settings' diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.routing.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.routing.yml new file mode 100644 index 00000000..e9fd9ac1 --- /dev/null +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.routing.yml @@ -0,0 +1,7 @@ +system.islandora_breadcrumbs_settings: + path: '/admin/config/islandora/breadcrumbs' + defaults: + _form: 'Drupal\islandora_breadcrumbs\Form\IslandoraBreadcrumbsSettingsForm' + _title: 'Islandora Breadcrumbs Settings' + requirements: + _permission: 'administer site configuration' diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml index 58e3c959..71c723f0 100644 --- a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml @@ -1,6 +1,6 @@ services: islandora_breadcrumbs.breadcrumb: class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder - arguments: ['@entity_type.manager', '@config.factory'] + arguments: ['@entity_type.manager', '@config.factory', '@islandora.utils'] tags: - { name: breadcrumb_builder, priority: 100 } diff --git a/modules/islandora_breadcrumbs/src/Form/IslandoraBreadcrumbsSettingsForm.php b/modules/islandora_breadcrumbs/src/Form/IslandoraBreadcrumbsSettingsForm.php new file mode 100644 index 00000000..679a565e --- /dev/null +++ b/modules/islandora_breadcrumbs/src/Form/IslandoraBreadcrumbsSettingsForm.php @@ -0,0 +1,132 @@ +config(static::SETTINGS); + + $form['maxDepth'] = [ + '#type' => 'number', + '#default_value' => $config->get('maxDepth'), + '#min' => -1, + '#step' => 1, + '#title' => $this->t('Maximum number of ancestor breadcrumbs'), + '#description' => $this->t("Stops adding ancestor references when the chain reaches this number. The count does not include the current node when enabled. The default value, '-1' disables this feature."), + ]; + + $form['includeSelf'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Include the current node in the breadcrumbs?'), + '#default_value' => $config->get('includeSelf'), + ]; + + // Using the textarea instead of a select so the site maintainer can + // provide an ordered list of items rather than simply selecting from a + // list which enforces it's own order. + $form['referenceFields'] = [ + '#type' => 'textarea', + '#title' => $this->t('Entity Reference fields to follow'), + '#default_value' => implode("\n", $config->get('referenceFields')), + '#description' => $this->t("Entity Reference field machine names to follow when building the breadcrumbs.
    One per line.
    Valid options: @options", + [ + "@options" => implode(", ", static::getNodeEntityReferenceFields()), + ] + ), + '#element_validate' => [[get_class($this), 'validateReferenceFields']], + + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * Returns a list of node entity reference field machine names. + * + * We use this for building the form field description and for + * validating the reference fields value. + */ + protected static function getNodeEntityReferenceFields() { + return array_keys(\Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference')['node']); + } + + /** + * Turns a text area into an array of values. + * + * Used for validating the field reference text area + * and saving the form state. + */ + protected static function textToArray($string) { + return array_filter(array_map('trim', explode("\n", $string)), 'strlen'); + } + + /** + * Callback for settings form. + * + * @param array $element + * An associative array containing the properties and children of the + * generic form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form for the form this element belongs to. + * + * @see \Drupal\Core\Render\Element\FormElement::processPattern() + */ + public static function validateReferenceFields(array $element, FormStateInterface $form_state) { + + $valid_fields = static::getNodeEntityReferenceFields(); + + foreach (static::textToArray($element['#value']) as $value) { + if (!in_array($value, $valid_fields)) { + $form_state->setError($element, t('"@field" is not a valid entity reference field!', ["@field" => $value])); + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->configFactory->getEditable(static::SETTINGS) + ->set('referenceFields', static::textToArray($form_state->getValue('referenceFields'))) + ->set('maxDepth', $form_state->getValue('maxDepth')) + ->set('includeSelf', $form_state->getValue('includeSelf')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php index 93ed7097..5ed7f6d0 100644 --- a/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php +++ b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php @@ -4,12 +4,12 @@ namespace Drupal\islandora_breadcrumbs; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\islandora\IslandoraUtils; /** * Provides breadcrumbs for nodes using a configured entity reference field. @@ -31,6 +31,13 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { */ protected $nodeStorage; + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + /** * Constructs a breadcrumb builder. * @@ -38,10 +45,13 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { * Storage to load nodes. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The configuration factory. + * @param \Drupal\islandora\IslandoraUtils $utils + * Islandora utils service. */ - public function __construct(EntityTypeManagerInterface $entity_manager, ConfigFactoryInterface $config_factory) { + public function __construct(EntityTypeManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, IslandoraUtils $utils) { $this->nodeStorage = $entity_manager->getStorage('node'); $this->config = $config_factory->get('islandora_breadcrumbs.breadcrumbs'); + $this->utils = $utils; } /** @@ -54,7 +64,14 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { $nid = $attributes->getRawParameters()->get('node'); if (!empty($nid)) { $node = $this->nodeStorage->load($nid); - return (!empty($node) && $node->hasField($this->config->get('referenceField'))); + if (empty($node)) { + return FALSE; + } + foreach ($this->config->get('referenceFields') as $field) { + if ($node->hasField($field)) { + return TRUE; + } + } } } @@ -66,51 +83,29 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { $nid = $route_match->getRawParameters()->get('node'); $node = $this->nodeStorage->load($nid); $breadcrumb = new Breadcrumb(); + $breadcrumb->addCacheableDependency($this->config); $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '')); - $chain = []; - $this->walkMembership($node, $chain); + $chain = array_reverse($this->utils->findAncestors($node, $this->config->get('referenceFields'), $this->config->get('maxDepth'))); - if (!$this->config->get('includeSelf')) { - array_pop($chain); + // XXX: Handle a looping breadcrumb scenario by filtering the present + // node out and then optionally re-adding it after if set to do so. + $chain = array_filter($chain, function ($link) use ($nid) { + return $link !== $nid; + }); + if ($this->config->get('includeSelf')) { + array_push($chain, $nid); } $breadcrumb->addCacheableDependency($node); // Add membership chain to the breadcrumb. foreach ($chain as $chainlink) { - $breadcrumb->addCacheableDependency($chainlink); - $breadcrumb->addLink($chainlink->toLink()); + $node = $this->nodeStorage->load($chainlink); + $breadcrumb->addCacheableDependency($node); + $breadcrumb->addLink($node->toLink()); } $breadcrumb->addCacheContexts(['route']); return $breadcrumb; } - /** - * Follows chain of field_member_of links. - * - * We pass crumbs by reference to enable checking for looped chains. - */ - protected function walkMembership(EntityInterface $entity, &$crumbs) { - // Avoid infinate loops, return if we've seen this before. - foreach ($crumbs as $crumb) { - if ($crumb->uuid == $entity->uuid) { - return; - } - } - - // Add this item onto the pile. - array_unshift($crumbs, $entity); - - if ($this->config->get('maxDepth') > 0 && count($crumbs) >= $this->config->get('maxDepth')) { - return; - } - - // Find the next in the chain, if there are any. - if ($entity->hasField($this->config->get('referenceField')) && - !$entity->get($this->config->get('referenceField'))->isEmpty() && - $entity->get($this->config->get('referenceField'))->entity instanceof EntityInterface) { - $this->walkMembership($entity->get($this->config->get('referenceField'))->entity, $crumbs); - } - } - } diff --git a/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php b/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php index 80f5dbee..ee35a1ed 100644 --- a/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php +++ b/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php @@ -20,7 +20,7 @@ class BreadcrumbsTest extends IslandoraFunctionalTestBase { * * @var array */ - public static $modules = [ + protected static $modules = [ 'islandora_breadcrumbs', ]; @@ -56,7 +56,7 @@ class BreadcrumbsTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Create some nodes. diff --git a/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml b/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml index 97619cd2..4976d1d6 100644 --- a/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml +++ b/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml @@ -14,6 +14,8 @@ module: core locked: false cardinality: 1 translatable: true -indexes: { } +indexes: + value: + - value persist_with_no_fields: false custom_storage: false diff --git a/modules/islandora_core_feature/config/install/filehash.settings.yml b/modules/islandora_core_feature/config/install/filehash.settings.yml index aa9c188d..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' -dedupe: false + 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.all_taxonomy_terms.yml b/modules/islandora_core_feature/config/install/views.view.all_taxonomy_terms.yml index 56b45066..8c3cb0f3 100644 --- a/modules/islandora_core_feature/config/install/views.view.all_taxonomy_terms.yml +++ b/modules/islandora_core_feature/config/install/views.view.all_taxonomy_terms.yml @@ -168,4 +168,3 @@ display: - url.query_args - user.permissions tags: { } - 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..e529f21e 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,9 @@ display: cache_metadata: max-age: -1 contexts: + - 'languages:language_content' - 'languages:language_interface' - request_format - url + - user.permissions tags: { } diff --git a/modules/islandora_core_feature/config/install/views.view.manage_members.yml b/modules/islandora_core_feature/config/install/views.view.manage_members.yml index a978f1d2..4a242924 100644 --- a/modules/islandora_core_feature/config/install/views.view.manage_members.yml +++ b/modules/islandora_core_feature/config/install/views.view.manage_members.yml @@ -1,15 +1,17 @@ langcode: en status: true dependencies: - enforced: - module: - - islandora_core_feature module: - jsonld - node - rest - serialization - user + enforced: + module: + - islandora_core_feature +_core: + default_config_hash: Zwu8JUsBiYaxPko_9DbJJhA-ZZSGOm81I9XtT9NH3M4 id: manage_members label: 'Manage members' module: views @@ -17,61 +19,14 @@ description: 'Manage members belonging to a piece of content' tag: '' base_table: node_field_data base_field: nid -core: 8.x display: default: - display_plugin: default id: default display_title: Master + display_plugin: default position: 0 display_options: - access: - type: perm - options: - perm: 'manage members' - 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: table - row: - type: fields + title: 'Manage members' fields: node_bulk_form: id: node_bulk_form @@ -80,6 +35,8 @@ display: relationship: none group_type: group admin_label: '' + entity_type: node + plugin_id: node_bulk_form label: 'Node operations bulk form' exclude: false alter: @@ -124,33 +81,27 @@ display: action_title: Action include_exclude: exclude selected_actions: { } - entity_type: node - plugin_id: node_bulk_form title: id: title table: node_field_data field: title + relationship: none + group_type: group + admin_label: '' entity_type: node entity_field: title + plugin_id: field + label: Title + exclude: false alter: alter_text: false make_link: false absolute: false - trim: false word_boundary: false ellipsis: false strip_tags: false + trim: false html: false - hide_empty: false - empty_zero: false - settings: - link_to_entity: true - plugin_id: field - relationship: none - group_type: group - admin_label: '' - label: Title - exclude: false element_type: '' element_class: '' element_label_type: '' @@ -160,9 +111,13 @@ display: element_wrapper_class: '' element_default_classes: true empty: '' + hide_empty: false + empty_zero: false hide_alter_empty: true click_sort_column: value type: string + settings: + link_to_entity: true group_column: value group_columns: { } group_rows: true @@ -180,6 +135,8 @@ display: relationship: none group_type: group admin_label: '' + entity_type: node + plugin_id: entity_operations label: 'Operations links' exclude: false alter: @@ -222,15 +179,56 @@ display: empty_zero: false hide_alter_empty: true destination: false - entity_type: node - plugin_id: entity_operations - filters: { } - sorts: { } - title: 'Manage members' - header: { } - footer: { } + 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: 'manage members' + cache: + type: tag + options: { } empty: { } - relationships: { } + sorts: + field_weight_value: + id: field_weight_value + table: node__field_weight + field: field_weight_value + relationship: none + group_type: group + admin_label: '' + plugin_id: standard + order: ASC + expose: + label: '' + field_identifier: '' + exposed: false arguments: field_member_of_target_id: id: field_member_of_target_id @@ -239,6 +237,7 @@ display: relationship: none group_type: group admin_label: '' + plugin_id: numeric default_action: default exception: value: all @@ -252,8 +251,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 @@ -263,17 +262,32 @@ display: type: 'entity:node' fail: 'not found' validate_options: - operation: view - multiple: 0 bundles: { } access: false + operation: view + multiple: 0 break_phrase: false not: false - plugin_id: numeric - display_extenders: { } + filters: { } filter_groups: operator: AND groups: { } + style: + type: table + row: + type: fields + 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: 0 contexts: @@ -285,9 +299,9 @@ display: - user.permissions tags: { } page_1: - display_plugin: page id: page_1 display_title: Page + display_plugin: page position: 1 display_options: display_extenders: { } @@ -296,11 +310,11 @@ display: type: tab title: Children description: '' + weight: 0 expanded: false + menu_name: main parent: '' - weight: 0 context: '0' - menu_name: main cache_metadata: max-age: 0 contexts: @@ -312,13 +326,11 @@ display: - 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: node/%node/members style: type: serializer options: @@ -326,6 +338,8 @@ display: formats: jsonld: jsonld json: json + display_extenders: { } + path: node/%node/members auth: - basic_auth - jwt_auth diff --git a/modules/islandora_core_feature/config/install/views.view.non_fedora_files.yml b/modules/islandora_core_feature/config/install/views.view.non_fedora_files.yml index 88b0f308..b90494f5 100644 --- a/modules/islandora_core_feature/config/install/views.view.non_fedora_files.yml +++ b/modules/islandora_core_feature/config/install/views.view.non_fedora_files.yml @@ -194,4 +194,3 @@ display: - url.query_args - user.permissions tags: { } - diff --git a/modules/islandora_core_feature/islandora_core_feature.info.yml b/modules/islandora_core_feature/islandora_core_feature.info.yml index bf4f8d7a..6976eb8c 100755 --- a/modules/islandora_core_feature/islandora_core_feature.info.yml +++ b/modules/islandora_core_feature/islandora_core_feature.info.yml @@ -1,8 +1,7 @@ name: 'Islandora Core Feature' description: 'Minimum configuration required for Islandora.' type: module -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 dependencies: - drupal:basic_auth - drupal:content_translation diff --git a/modules/islandora_core_feature/islandora_core_feature.post_update.php b/modules/islandora_core_feature/islandora_core_feature.post_update.php new file mode 100644 index 00000000..10547231 --- /dev/null +++ b/modules/islandora_core_feature/islandora_core_feature.post_update.php @@ -0,0 +1,20 @@ +getStorage('field_storage_config'); + $field = $storage->load('node.field_weight'); + $indexes = $field->getIndexes(); + $indexes += [ + 'value' => ['value'], + ]; + $field->setIndexes($indexes); + $field->save(); +} diff --git a/modules/islandora_iiif/README.md b/modules/islandora_iiif/README.md index ab06524b..c1f89872 100644 --- a/modules/islandora_iiif/README.md +++ b/modules/islandora_iiif/README.md @@ -1,4 +1,4 @@ -# Islandora IIIF +# Islandora IIIF [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg?style=flat-square)](https://php.net/) [![Contribution Guidelines](http://img.shields.io/badge/CONTRIBUTING-Guidelines-blue.svg)](./CONTRIBUTING.md) @@ -11,7 +11,7 @@ Provides IIIF manifests using views. ## Requirements - `islandora` and `islandora_core_feature` -- A IIIF image server (such as Cantaloupe) +- A IIIF image server (such as Cantaloupe) ## Installation @@ -32,6 +32,12 @@ You can set the following configuration at `admin/config/islandora/iiif`: - IIIF Image server location - The URL to your IIIF image server (without trailing slash). +### Views Style Plugin + +This module implements a Views Style plugin. It provides the following settings: + +1. Tile Source: A field that was added to the views list of fields with the image to be served. This should be a File or Image type field on a Media. +2. Structured Text field: This lets you specify a file field where OCR text with positional data, e.g., hOCR can be found. ## Documentation Official documentation is available on the [Islandora 8 documentation site](https://islandora.github.io/documentation/). diff --git a/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml index 6ef42bc4..f9e870ef 100644 --- a/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml +++ b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml @@ -5,3 +5,14 @@ islandora_iiif.settings: iiif_server: type: string label: 'IIIF Server Url' + use_relative_paths: + type: boolean + label: 'Use relative paths in manifest.' + +views.style.iiif_manifest: + type: views_style + mapping: + iiif_tile_field: + type: sequence + sequence: + type: string diff --git a/modules/islandora_iiif/islandora_iiif.info.yml b/modules/islandora_iiif/islandora_iiif.info.yml index 0492158a..39b835c0 100644 --- a/modules/islandora_iiif/islandora_iiif.info.yml +++ b/modules/islandora_iiif/islandora_iiif.info.yml @@ -1,8 +1,7 @@ name: 'Islandora IIIF' type: module description: 'IIIF support for Islandora' -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 package: Islandora dependencies: - drupal:islandora diff --git a/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php b/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php index dc750a5a..a99539a3 100644 --- a/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php +++ b/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php @@ -72,7 +72,18 @@ class IslandoraIIIFConfigForm extends ConfigFormBase { '#title' => $this->t('IIIF Image server location'), '#description' => $this->t('Please enter the image server location without trailing slash. e.g. http://www.example.org/iiif/2.'), '#default_value' => $config->get('iiif_server'), + '#config' => [ + 'key' => 'islandora_iiif.settings:iiif_server', + ], ]; + + $form['use_relative_paths'] = [ + '#type' => 'checkbox', + '#title' => $this->t("Use relative file paths in manifest."), + '#description' => $this->t("Check this if your IIIF Server is configured to access files locally. If unchecked, the absolute URL will be given and the IIIF server will make requests to this site to retrieve images."), + '#default_value' => $config->get('use_relative_paths'), + ]; + return parent::buildForm($form, $form_state); } @@ -99,6 +110,7 @@ class IslandoraIIIFConfigForm extends ConfigFormBase { $this->config('islandora_iiif.settings') ->set('iiif_server', $form_state->getValue('iiif_server')) + ->set('use_relative_paths', $form_state->getValue('use_relative_paths')) ->save(); } diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php index 073ca04e..804153ab 100644 --- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -2,19 +2,24 @@ namespace Drupal\islandora_iiif\Plugin\views\style; -use Drupal\views\Plugin\views\style\StylePluginBase; +use Drupal\Core\Config\ImmutableConfig; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Url; +use Drupal\views\Plugin\views\style\StylePluginBase; use Drupal\views\ResultRow; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\HttpFoundation\Request; -use Drupal\Core\Config\ImmutableConfig; -use Drupal\Core\File\FileSystemInterface; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\ServerException; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\HttpFoundation\Request; /** * Provide serializer format for IIIF Manifest. @@ -68,6 +73,13 @@ class IIIFManifest extends StylePluginBase { */ protected $iiifConfig; + /** + * The Drupal Entity Type Manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * The Drupal Filesystem. * @@ -75,6 +87,13 @@ class IIIFManifest extends StylePluginBase { */ protected $fileSystem; + /** + * The Guzzle HTTP Client. + * + * @var \GuzzleHttp\Client + */ + protected $httpClient; + /** * The messenger. * @@ -82,18 +101,27 @@ class IIIFManifest extends StylePluginBase { */ protected $messenger; + /** + * Module Handler for running hooks. + * + * @var \Drupal\Core\Extention\ModuleHandlerInterface + */ + protected $moduleHandler; + /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, Request $request, ImmutableConfig $iiif_config, FileSystemInterface $file_system, Client $http_client, MessengerInterface $messenger) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, Request $request, ImmutableConfig $iiif_config, EntityTypeManagerInterface $entity_type_manager, FileSystemInterface $file_system, Client $http_client, MessengerInterface $messenger, ModuleHandlerInterface $moduleHandler) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->serializer = $serializer; $this->request = $request; $this->iiifConfig = $iiif_config; + $this->entityTypeManager = $entity_type_manager; $this->fileSystem = $file_system; $this->httpClient = $http_client; $this->messenger = $messenger; + $this->moduleHandler = $moduleHandler; } /** @@ -107,12 +135,24 @@ class IIIFManifest extends StylePluginBase { $container->get('serializer'), $container->get('request_stack')->getCurrentRequest(), $container->get('config.factory')->get('islandora_iiif.settings'), + $container->get('entity_type.manager'), $container->get('file_system'), $container->get('http_client'), - $container->get('messenger') + $container->get('messenger'), + $container->get('module_handler') ); } + /** + * Return the request property. + * + * @return \Symfony\Component\HttpFoundation\Request + * The Symfony request object + */ + public function getRequest() { + return $this->request; + } + /** * {@inheritdoc} */ @@ -121,18 +161,21 @@ class IIIFManifest extends StylePluginBase { $iiif_address = $this->iiifConfig->get('iiif_server'); if (!is_null($iiif_address) && !empty($iiif_address)) { // Get the current URL being requested. - $request_url = $this->request->getSchemeAndHttpHost() . $this->request->getRequestUri(); + $request_host = $this->request->getSchemeAndHttpHost(); + $request_url = $this->request->getRequestUri(); // Strip off the last URI component to get the base ID of the URL. // @todo assumming the view is a path like /node/1/manifest.json - $url_components = explode('/', $request_url); + $url_components = explode('/', trim($request_url, '/')); array_pop($url_components); - $iiif_base_id = implode('/', $url_components); + $content_path = implode('/', $url_components); + $iiif_base_id = $request_host . '/' . $content_path; + // @see https://iiif.io/api/presentation/2.1/#manifest $json += [ '@type' => 'sc:Manifest', '@id' => $request_url, // If the View has a title, set the View title as the manifest label. - 'label' => $this->view->getTitle() ?: 'IIIF Manifest', + 'label' => $this->view->getTitle() ?: $this->getEntityTitle($content_path), '@context' => 'http://iiif.io/api/presentation/2/context.json', // @see https://iiif.io/api/presentation/2.1/#sequence 'sequences' => [ @@ -156,6 +199,9 @@ class IIIFManifest extends StylePluginBase { $content_type = 'json'; + // Give other modules a chance to alter the manifest. + $this->moduleHandler->alter('islandora_iiif_manifest', $json, $this); + return $this->serializer->serialize($json, $content_type, ['views_style_plugin' => $this]); } @@ -175,18 +221,28 @@ class IIIFManifest extends StylePluginBase { */ protected function getTileSourceFromRow(ResultRow $row, $iiif_address, $iiif_base_id) { $canvases = []; - foreach ($this->options['iiif_tile_field'] as $iiif_tile_field) { + foreach (array_filter(array_values($this->options['iiif_tile_field'])) as $iiif_tile_field) { $viewsField = $this->view->field[$iiif_tile_field]; $entity = $viewsField->getEntity($row); if (isset($entity->{$viewsField->definition['field_name']})) { - /** @var \Drupal\Core\Field\FieldItemListInterface $images */ $images = $entity->{$viewsField->definition['field_name']}; - foreach ($images as $image) { + foreach ($images as $i => $image) { + if (!$image->entity->access('view')) { + // If the user does not have permission to view the file, skip it. + continue; + } + // Create the IIIF URL for this file // Visiting $iiif_url will resolve to the info.json for the image. - $file_url = $image->entity->createFileUrl(FALSE); + if ($this->iiifConfig->get('use_relative_paths')) { + $file_url = ltrim($image->entity->createFileUrl(TRUE), '/'); + } + else { + $file_url = $image->entity->createFileUrl(FALSE); + } + $mime_type = $image->entity->getMimeType(); $iiif_url = rtrim($iiif_address, '/') . '/' . urlencode($file_url); @@ -194,37 +250,9 @@ class IIIFManifest extends StylePluginBase { $canvas_id = $iiif_base_id . '/canvas/' . $entity->id(); $annotation_id = $iiif_base_id . '/annotation/' . $entity->id(); - // Try to fetch the IIIF metadata for the image. - try { - $info_json = $this->httpClient->get($iiif_url)->getBody(); - $resource = json_decode($info_json, TRUE); - $width = $resource['width']; - $height = $resource['height']; - } - catch (ClientException | ServerException | ConnectException $e) { - // If we couldn't get the info.json from IIIF - // try seeing if we can get it from Drupal. - if (empty($width) || empty($height)) { - // Get the image properties so we know the image width/height. - $properties = $image->getProperties(); - $width = isset($properties['width']) ? $properties['width'] : 0; - $height = isset($properties['height']) ? $properties['height'] : 0; - - // If this is a TIFF AND we don't know the width/height - // see if we can get the image size via PHP's core function. - if ($mime_type === 'image/tiff' && !$width || !$height) { - $uri = $image->entity->getFileUri(); - $path = $this->fileSystem->realpath($uri); - $image_size = getimagesize($path); - if ($image_size) { - $width = $image_size[0]; - $height = $image_size[1]; - } - } - } - } + [$width, $height] = $this->getCanvasDimensions($iiif_url, $image, $mime_type); - $canvases[] = [ + $tmp_canvas = [ // @see https://iiif.io/api/presentation/2.1/#canvas '@id' => $canvas_id, '@type' => 'sc:Canvas', @@ -253,6 +281,24 @@ class IIIFManifest extends StylePluginBase { ], ], ]; + + if ($ocr_url = $this->getOcrUrl($entity, $row, $i)) { + $tmp_canvas['seeAlso'] = [ + '@id' => $ocr_url, + 'format' => 'text/vnd.hocr+html', + 'profile' => 'http://kba.cloud/hocr-spec', + 'label' => 'hOCR embedded text', + ]; + } + + // Give other modules a chance to alter the canvas. + $alter_options = [ + 'options' => $this->options, + 'views_plugin' => $this, + ]; + $this->moduleHandler->alter('islandora_iiif_manifest_canvas', $tmp_canvas, $row, $alter_options); + + $canvases[] = $tmp_canvas; } } } @@ -260,6 +306,118 @@ class IIIFManifest extends StylePluginBase { return $canvases; } + /** + * Try to fetch the IIIF metadata for the image. + * + * @param string $iiif_url + * Base URL of the canvas. + * @param \Drupal\Core\Field\FieldItemInterface $image + * The image field. + * @param string $mime_type + * The mime type of the image. + * + * @return [string] + * The width and height of the image. + */ + protected function getCanvasDimensions(string $iiif_url, FieldItemInterface $image, string $mime_type) { + + if (isset($image->width) && is_numeric($image->width) + && isset($image->height) && is_numeric($image->height)) { + return [intval($image->width), intval($image->height)]; + } + + try { + $info_json = $this->httpClient->get($iiif_url)->getBody(); + $resource = json_decode($info_json, TRUE); + $width = $resource['width']; + $height = $resource['height']; + } + catch (ClientException | ServerException | ConnectException $e) { + // If we couldn't get the info.json from IIIF + // try seeing if we can get it from Drupal. + if (empty($width) || empty($height)) { + // Get the image properties so we know the image width/height. + $properties = $image->getProperties(); + $width = isset($properties['width']) ? $properties['width'] : 0; + $height = isset($properties['height']) ? $properties['height'] : 0; + + // If this is a TIFF AND we don't know the width/height + // see if we can get the image size via PHP's core function. + if ($mime_type === 'image/tiff' && !$width || !$height) { + $uri = $image->entity->getFileUri(); + $path = $this->fileSystem->realpath($uri); + $image_size = getimagesize($path); + if ($image_size) { + $width = $image_size[0]; + $height = $image_size[1]; + } + } + } + } + return [$width, $height]; + } + + /** + * Retrieves a URL text with positional data such as hOCR. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity at the current row. + * @param \Drupal\views\ResultRow $row + * Result row. + * @param int $delta + * The delta in case there are multiple canvases on one media. + * + * @return string|false + * The absolute URL of the current row's structured text, + * or FALSE if none. + */ + protected function getOcrUrl(EntityInterface $entity, ResultRow $row, $delta) { + $ocr_url = FALSE; + $iiif_ocr_file_field = !empty($this->options['iiif_ocr_file_field']) ? array_filter(array_values($this->options['iiif_ocr_file_field'])) : []; + $ocrField = count($iiif_ocr_file_field) > 0 ? $this->view->field[$iiif_ocr_file_field[0]] : NULL; + if ($ocrField) { + $ocr_entity = $ocrField->getEntity($row); + $ocr_field_name = $ocrField->definition['field_name']; + if (!is_null($ocr_field_name)) { + $ocrs = $ocr_entity->{$ocr_field_name}; + $ocr = isset($ocrs[$delta]) ? $ocrs[$delta] : FALSE; + if ($ocr) { + $ocr_url = $ocr->entity->createFileUrl(FALSE); + } + } + } + + return $ocr_url; + } + + /** + * Pull a title from the node or media passed to this view. + * + * @param string $content_path + * The path of the content being requested. + * + * @return string + * The entity's title. + */ + public function getEntityTitle(string $content_path): string { + $entity_title = $this->t('IIIF Manifest'); + try { + $params = Url::fromUserInput($content_path)->getRouteParameters(); + if (isset($params['node'])) { + $node = $this->entityTypeManager->getStorage('node')->load($params['node']); + $entity_title = $node->getTitle(); + } + elseif (isset($params['media'])) { + $media = $this->entityTypeManager->getStorage('media')->load($params['media']); + $entity_title = $media->getName(); + } + } + catch (\InvalidArgumentException $e) { + + } + return $entity_title; + } + /** * {@inheritdoc} */ @@ -267,6 +425,7 @@ class IIIFManifest extends StylePluginBase { $options = parent::defineOptions(); $options['iiif_tile_field'] = ['default' => '']; + $options['iiif_ocr_file_field'] = ['default' => '']; return $options; } @@ -322,6 +481,15 @@ class IIIFManifest extends StylePluginBase { // otherwise could lock up the form when setting up a View. '#required' => count($field_options) > 0, ]; + + $form['iiif_ocr_file_field'] = [ + '#title' => $this->t('Structured OCR data file field'), + '#type' => 'checkboxes', + '#default_value' => $this->options['iiif_ocr_file_field'], + '#description' => $this->t('The source of structured OCR text for each entity.'), + '#options' => $field_options, + '#required' => FALSE, + ]; } /** diff --git a/modules/islandora_image/islandora_image.info.yml b/modules/islandora_image/islandora_image.info.yml index 87076462..277966f3 100644 --- a/modules/islandora_image/islandora_image.info.yml +++ b/modules/islandora_image/islandora_image.info.yml @@ -1,8 +1,7 @@ name: 'Islandora Image' type: module description: 'Islandora Image derivative actions' -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 package: Islandora dependencies: - drupal:islandora 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/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php b/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php index b6e016fc..44cdda58 100644 --- a/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php +++ b/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php @@ -68,9 +68,10 @@ class GenerateImageDerivativeTest extends GenerateDerivativeTestBase { 'name[0][value]' => 'Test Media', 'files[field_media_file_0]' => __DIR__ . '/../../fixtures/test_file.txt', 'field_media_of[0][target_id]' => 'Test Node', - 'field_tags[0][target_id]' => 'Preservation Master', + 'field_media_use[0][target_id]' => $this->preservationMasterTerm->label(), ]; - $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, $this->t('Save')); + $this->drupalGet('media/add/' . $this->testMediaType->id()); + $this->submitForm($values, $this->t('Save')); $expected = [ 'source_uri' => 'test_file.txt', diff --git a/modules/islandora_text_extraction/islandora_text_extraction.info.yml b/modules/islandora_text_extraction/islandora_text_extraction.info.yml index 67687f90..fb768e2a 100644 --- a/modules/islandora_text_extraction/islandora_text_extraction.info.yml +++ b/modules/islandora_text_extraction/islandora_text_extraction.info.yml @@ -1,8 +1,7 @@ name: 'Islandora Text Extraction' type: module description: 'Islandora 8 module to connect to Hypercube microservice, and to get text from PDF ingest' -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 package: 'Islandora' dependencies: - drupal:islandora diff --git a/modules/islandora_text_extraction/islandora_text_extraction.module b/modules/islandora_text_extraction/islandora_text_extraction.module index 5d6f6437..ca330dd4 100644 --- a/modules/islandora_text_extraction/islandora_text_extraction.module +++ b/modules/islandora_text_extraction/islandora_text_extraction.module @@ -40,6 +40,10 @@ function islandora_text_extraction_media_presave(MediaInterface $media) { $file = File::load($file_id); if ($file) { $data = file_get_contents($file->getFileUri()); + // Check if it's already markup like hOCR. + if (substr($data, 0, 5) == 'set('field_edited_text', $data); $media->field_edited_text->format = 'basic_html'; diff --git a/modules/islandora_text_extraction/src/Controller/MediaSourceController.php b/modules/islandora_text_extraction/src/Controller/MediaSourceController.php index 14c36ebd..6b886308 100644 --- a/modules/islandora_text_extraction/src/Controller/MediaSourceController.php +++ b/modules/islandora_text_extraction/src/Controller/MediaSourceController.php @@ -5,6 +5,7 @@ namespace Drupal\islandora_text_extraction\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\File\FileSystem; use Drupal\Core\File\FileSystemInterface; +use Drupal\file\FileRepository; use Drupal\media\Entity\Media; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -42,14 +43,24 @@ class MediaSourceController extends ControllerBase { */ protected $fileSystem; + /** + * File repository service. + * + * @var \Drupal\file\FileRepository + */ + protected $fileRepository; + /** * MediaSourceController constructor. * * @param \Drupal\Core\File\FileSystem $fileSystem * Filesystem service. + * @param \Drupal\file\FileRepository $fileRepository + * File Repository service. */ - public function __construct(FileSystem $fileSystem) { + public function __construct(FileSystem $fileSystem, FileRepository $fileRepository) { $this->fileSystem = $fileSystem; + $this->fileRepository = $fileRepository; } /** @@ -63,7 +74,8 @@ class MediaSourceController extends ControllerBase { */ public static function create(ContainerInterface $container) { return new static( - $container->get('file_system') + $container->get('file_system'), + $container->get('file.repository'), ); } @@ -98,7 +110,7 @@ class MediaSourceController extends ControllerBase { if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::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, FileSystemInterface::EXISTS_REPLACE); + $file = $this->fileRepository->writeData($contents, $content_location, FileSystemInterface::EXISTS_REPLACE); if ($media->hasField($destination_field)) { $media->{$destination_field}->setValue([ 'target_id' => $file->id(), @@ -108,7 +120,11 @@ class MediaSourceController extends ControllerBase { $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)); + // @todo The request actually has a malformed parameter string, ?text_format=plain_text?connection_close=true. + if (substr($request->query->get('text_format'), 0, 10) == 'plain_text') { + $contents = nl2br($contents); + } + $media->{$destination_text_field}->setValue($contents); } else { $this->getLogger('islandora')->warning("Field $destination_text_field is not defined in Media Type {$media->bundle()}"); diff --git a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php index 745c5772..272e9f01 100644 --- a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php +++ b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php @@ -22,9 +22,13 @@ class GenerateOCRDerivative extends AbstractGenerateDerivative { public function defaultConfiguration() { $config = parent::defaultConfiguration(); $config['path'] = '[date:custom:Y]-[date:custom:m]/[node:nid]-[term:name].txt'; - $config['mimetype'] = 'application/xml'; + $config['event'] = 'Generate Derivative'; + $config['source_term_uri'] = 'http://pcdm.org/use#OriginalFile'; + $config['derivative_term_uri'] = 'http://pcdm.org/use#ExtractedText'; + $config['mimetype'] = 'text/plain'; $config['queue'] = 'islandora-connector-ocr'; - $config['destination_media_type'] = 'file'; + $config['destination_media_type'] = 'extracted_text'; + $config['scheme'] = 'fedora'; return $config; } @@ -33,11 +37,9 @@ class GenerateOCRDerivative extends AbstractGenerateDerivative { */ 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...)'); - $form['mimetype']['#value'] = 'text/plain'; - $form['mimetype']['#type'] = 'textfield'; - unset($form['args']); + $form['args']['#description'] = $this->t("Arguments to send to Tesseract. To generate hOCR, use:
    -c tessedit_create_hocr=1 -c hocr_font_info=0"); + return $form; } diff --git a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php index 3b5e8498..565d7564 100644 --- a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php +++ b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php @@ -8,11 +8,11 @@ use Drupal\Core\Url; use Drupal\islandora\Plugin\Action\AbstractGenerateDerivativeMediaFile; /** - * Emits a Node for generating fits derivatives event. + * Generates OCR derivatives event. * * @Action( * id = "generate_extracted_text_file", - * label = @Translation("Generate an Extracted Text derivative file"), + * label = @Translation("Generate Extracted Text for Media Attachment"), * type = "media" * ) */ @@ -29,6 +29,7 @@ class GenerateOCRDerivativeFile extends AbstractGenerateDerivativeMediaFile { $config['destination_media_type'] = 'file'; $config['scheme'] = $this->config->get('default_scheme'); $config['destination_text_field_name'] = ''; + $config['text_format'] = 'plain_text'; return $config; } @@ -38,7 +39,7 @@ class GenerateOCRDerivativeFile extends AbstractGenerateDerivativeMediaFile { public function buildConfigurationForm(array $form, FormStateInterface $form_state) { $map = $this->entityFieldManager->getFieldMapByFieldType('text_long'); $file_fields = $map['media']; - $field_options = array_combine(array_keys($file_fields), array_keys($file_fields)); + $field_options = ['none' => $this->t('None')] + array_combine(array_keys($file_fields), array_keys($file_fields)); $form = parent::buildConfigurationForm($form, $form_state); $form['mimetype']['#description'] = $this->t('Mimetype to convert to (e.g. application/xml, etc...)'); $form['mimetype']['#value'] = 'text/plain'; @@ -48,13 +49,23 @@ class GenerateOCRDerivativeFile extends AbstractGenerateDerivativeMediaFile { $last = array_slice($form, count($form) - $position + 1); $middle['destination_text_field_name'] = [ - '#required' => TRUE, + '#required' => FALSE, '#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.'), ]; + $middle['text_format'] = [ + '#type' => 'select', + '#title' => $this->t('Format'), + '#options' => [ + 'plain_text' => $this->t('Plain text'), + 'hocr' => $this->t('hOCR text with positional data'), + ], + '#default_value' => $this->configuration['text_format'], + '#description' => $this->t("The type of text to be returned."), + ]; $form = array_merge($first, $middle, $last); unset($form['args']); @@ -81,17 +92,29 @@ class GenerateOCRDerivativeFile extends AbstractGenerateDerivativeMediaFile { 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'); + $this->configuration['text_format'] = $form_state->getValue('text_format'); + switch ($form_state->getValue('text_format')) { + case 'hocr': + $this->configuration['args'] = '-c tessedit_create_hocr=1 -c hocr_font_info=0'; + break; + + case 'plain_text': + $this->configuration['args'] = ''; + break; + } } /** * 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'], + 'text_format' => $this->configuration['text_format'], ]; $data['destination_uri'] = Url::fromRoute('islandora_text_extraction.attach_file_to_media', $route_params) ->setAbsolute() diff --git a/modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php b/modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php index 2e066943..055e76d9 100644 --- a/modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php +++ b/modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php @@ -132,8 +132,9 @@ class OcrTextFormatter extends FormatterBase implements ContainerFactoryPluginIn $fileItem = $item->getValue(); $file = $this->entityTypeManager->getStorage('file')->load($fileItem['target_id']); $contents = file_get_contents($file->getFileUri()); - if (mb_detect_encoding($contents) != 'UTF-8') { - $contents = utf8_encode($contents); + $detected_encoding = mb_detect_encoding($contents); + if ($detected_encoding != 'UTF-8') { + $contents = mb_convert_encoding($contents, 'UTF-8', $detected_encoding); } $contents = nl2br($contents); return $contents; diff --git a/modules/islandora_text_extraction/tests/src/Functional/LoadTest.php b/modules/islandora_text_extraction/tests/src/Functional/LoadTest.php index 31dca62c..172ae73a 100644 --- a/modules/islandora_text_extraction/tests/src/Functional/LoadTest.php +++ b/modules/islandora_text_extraction/tests/src/Functional/LoadTest.php @@ -17,7 +17,7 @@ class LoadTest extends IslandoraFunctionalTestBase { * * @var array */ - public static $modules = ['islandora_text_extraction']; + protected static $modules = ['islandora_text_extraction']; /** * A user with permission to administer site configuration. @@ -29,7 +29,7 @@ class LoadTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->user = $this->drupalCreateUser(['administer site configuration']); $this->drupalLogin($this->user); diff --git a/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml index 5b9aa14f..8596dbd3 100644 --- a/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml +++ b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml @@ -1,8 +1,7 @@ name: 'Islandora Text Extraction Defaults' type: module description: 'Default config for the Islandora Text Extraction module.' -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 package: Islandora dependencies: - drupal:field diff --git a/modules/islandora_video/config/schema/islandora_video.schema.yml b/modules/islandora_video/config/schema/islandora_video.schema.yml index b1d72d7f..01d28481 100644 --- a/modules/islandora_video/config/schema/islandora_video.schema.yml +++ b/modules/islandora_video/config/schema/islandora_video.schema.yml @@ -29,3 +29,6 @@ action.configuration.generate_video_derivative: path: type: text label: 'File path with extension' + +field.formatter.settings.islandora_file_video: + type: field.formatter.settings.file_video diff --git a/modules/islandora_video/islandora_video.info.yml b/modules/islandora_video/islandora_video.info.yml index 48eb82f2..aa51af11 100644 --- a/modules/islandora_video/islandora_video.info.yml +++ b/modules/islandora_video/islandora_video.info.yml @@ -2,7 +2,6 @@ name: 'Islandora Video' description: 'Islandora video derivative actions' type: module package: Islandora -core: 8.x -core_version_requirement: ^8 || ^9 +core_version_requirement: ^9 || ^10 dependencies: - drupal:islandora diff --git a/modules/islandora_video/templates/islandora-file-video.html.twig b/modules/islandora_video/templates/islandora-file-video.html.twig index 7c62bade..e1b8730c 100644 --- a/modules/islandora_video/templates/islandora-file-video.html.twig +++ b/modules/islandora_video/templates/islandora-file-video.html.twig @@ -21,7 +21,7 @@ {% endfor %} {% if tracks %} {% for track in tracks %} - {% endfor %} {% endif %} diff --git a/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php b/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php index 8714a2f1..de06ba2f 100644 --- a/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php +++ b/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php @@ -63,9 +63,10 @@ class GenerateVideoDerivativeTest extends GenerateDerivativeTestBase { 'name[0][value]' => 'Test Media', 'files[field_media_file_0]' => __DIR__ . '/../../fixtures/test_file.txt', 'field_media_of[0][target_id]' => 'Test Node', - 'field_tags[0][target_id]' => 'Preservation Master', + 'field_media_use[0][target_id]' => $this->preservationMasterTerm->label(), ]; - $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, $this->t('Save')); + $this->drupalGet('media/add/' . $this->testMediaType->id()); + $this->submitForm($values, $this->t('Save')); $expected = [ 'source_uri' => 'test_file.txt', diff --git a/phpunit.xml b/phpunit.xml index a4091781..46e82e78 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -58,7 +58,7 @@ ../modules/contrib/islandora/tests/src/Functional - ../modules/contrib/isladnora/modules/*/tests/src/Functional + ../modules/contrib/islandora/modules/*/tests/src/Functional ../modules/contrib/islandora/tests/src/FunctionalJavascript diff --git a/src/Commands/IslandoraCommands.php b/src/Commands/IslandoraCommands.php index 5209546c..bd47500a 100644 --- a/src/Commands/IslandoraCommands.php +++ b/src/Commands/IslandoraCommands.php @@ -4,7 +4,7 @@ namespace Drupal\islandora\Commands; use Consolidation\AnnotatedCommand\CommandData; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Session\AccountProxy; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Session\AccountSwitcherInterface; use Drupal\Core\Session\UserSession; use Drush\Commands\DrushCommands; @@ -39,7 +39,7 @@ class IslandoraCommands extends DrushCommands { /** * {@inheritdoc} */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountProxy $current_user, AccountSwitcherInterface $account_switcher) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user, AccountSwitcherInterface $account_switcher) { $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->accountSwitcher = $account_switcher; diff --git a/src/Controller/ManageMediaController.php b/src/Controller/ManageMediaController.php index 56e0b5c5..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 @field field.", [ + '@field' => $field, + ]), + 'manage_link' => $manage_link, + 'add_media' => $add_media_list, + ]; } /** @@ -45,13 +61,19 @@ class ManageMediaController extends ManageMembersController { * Whether we can or can't show the "thing". */ public function access(RouteMatch $route_match) { + // Route match is being used as opposed to slugs as there are a few + // admin routes being altered. + // @see: \Drupal\islandora\EventSubscriber\AdminViewsRouteSubscriber::alterRoutes(). if ($route_match->getParameters()->has('node')) { $node = $route_match->getParameter('node'); if (!$node instanceof NodeInterface) { $node = Node::load($node); } - if ($this->utils->isIslandoraType($node->getEntityTypeId(), $node->bundle())) { - return AccessResult::allowed(); + // Ensure there's actually a node before referencing it. + if ($node) { + if ($this->utils->isIslandoraType($node->getEntityTypeId(), $node->bundle())) { + return AccessResult::allowed(); + } } } return AccessResult::forbidden(); diff --git a/src/Controller/ManageMembersController.php b/src/Controller/ManageMembersController.php index 7f480fb3..c7b2df6a 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; @@ -28,7 +29,7 @@ class ManageMembersController extends EntityController { * * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ - protected $entityFieldManger; + protected $entityFieldManager; /** * The renderer. @@ -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 @field 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/Event/StompHeaderEvent.php b/src/Event/StompHeaderEvent.php index d6d93c22..1381a920 100644 --- a/src/Event/StompHeaderEvent.php +++ b/src/Event/StompHeaderEvent.php @@ -6,7 +6,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; use Symfony\Component\HttpFoundation\ParameterBag; -use Symfony\Component\EventDispatcher\Event; +use Drupal\Component\EventDispatcher\Event; /** * Event used to build headers for STOMP. diff --git a/src/EventGenerator/EmitEvent.php b/src/EventGenerator/EmitEvent.php index ebec711f..12700d73 100644 --- a/src/EventGenerator/EmitEvent.php +++ b/src/EventGenerator/EmitEvent.php @@ -5,6 +5,7 @@ namespace Drupal\islandora\EventGenerator; use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ConfigurableActionBase; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; @@ -13,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; @@ -67,6 +69,13 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact */ protected $messenger; + /** + * The logger. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + /** * Constructs a EmitEvent action. * @@ -88,6 +97,8 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact * The messenger. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * Event dispatcher service. + * @param \Drupal\Core\Logger\LoggerChannelInterface $channel + * Logger channel. */ public function __construct( array $configuration, @@ -98,7 +109,8 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact EventGeneratorInterface $event_generator, StatefulStomp $stomp, MessengerInterface $messenger, - EventDispatcherInterface $event_dispatcher + EventDispatcherInterface $event_dispatcher, + LoggerChannelInterface $channel ) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->account = $account; @@ -107,6 +119,7 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact $this->stomp = $stomp; $this->messenger = $messenger; $this->eventDispatcher = $event_dispatcher; + $this->logger = $channel; } /** @@ -122,7 +135,8 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact $container->get('islandora.eventgenerator'), $container->get('islandora.stomp'), $container->get('messenger'), - $container->get('event_dispatcher') + $container->get('event_dispatcher'), + $container->get('logger.channel.islandora') ); } @@ -132,12 +146,22 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact public function execute($entity = NULL) { // Generate event as stomp message. try { + if (is_null($this->stomp->getClient()->getProtocol())) { + // getProtocol() can return NULL but that causes a larger problem. + // So attempt to disconnect + connect to re-establish the connection or + // throw a StompException. + // @see https://github.com/stomp-php/stomp-php/issues/167 + // @see https://github.com/stomp-php/stomp-php/blob/3a9347a11743d0b79fd60564f356bc3efe40e615/src/Client.php#L429-L434 + $this->stomp->getClient()->disconnect(); + $this->stomp->getClient()->connect(); + } + $user = $this->entityTypeManager->getStorage('user')->load($this->account->id()); $data = $this->generateData($entity); $event = $this->eventDispatcher->dispatch( - StompHeaderEvent::EVENT_NAME, - new StompHeaderEvent($entity, $user, $data, $this->getConfiguration()) + new StompHeaderEvent($entity, $user, $data, $this->getConfiguration()), + StompHeaderEvent::EVENT_NAME ); $message = new Message( @@ -145,19 +169,27 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact $event->getHeaders()->all() ); } + catch (IslandoraDerivativeException $e) { + $this->logger->info($e->getMessage()); + return; + } catch (StompHeaderEventException $e) { - \Drupal::logger('islandora')->error($e->getMessage()); - $this->messenger->addMessage($e->getMessage(), 'error'); + $this->logger->error($e->getMessage()); + $this->messenger->addError($e->getMessage()); + return; + } + catch (StompException $e) { + $this->logger->error("Unable to connect to JMS Broker: @msg", ["@msg" => $e->getMessage()]); + $this->messenger->addWarning("Unable to connect to JMS Broker, items might not be synchronized to external services."); return; } catch (\RuntimeException $e) { // Notify the user the event couldn't be generated and abort. - \Drupal::logger('islandora')->error( + $this->logger->error( $this->t('Error generating event: @msg', ['@msg' => $e->getMessage()]) ); - $this->messenger->addMessage( - $this->t('Error generating event: @msg', ['@msg' => $e->getMessage()]), - 'error' + $this->messenger->addError( + $this->t('Error generating event: @msg', ['@msg' => $e->getMessage()]) ); return; } @@ -170,17 +202,16 @@ abstract class EmitEvent extends ConfigurableActionBase implements ContainerFact } catch (StompException $e) { // Log it. - \Drupal::logger('islandora')->error( + $this->logger->error( 'Error publishing message: @msg', ['@msg' => $e->getMessage()] ); // Notify user. - $this->messenger->addMessage( + $this->messenger->addError( $this->t('Error publishing message: @msg', ['@msg' => $e->getMessage()] - ), - 'error' + ) ); } } diff --git a/src/EventGenerator/EventGenerator.php b/src/EventGenerator/EventGenerator.php index 6b3a4c5c..4c29e44b 100644 --- a/src/EventGenerator/EventGenerator.php +++ b/src/EventGenerator/EventGenerator.php @@ -147,8 +147,19 @@ class EventGenerator implements EventGeneratorInterface { } } - unset($data["event"]); - unset($data["queue"]); + $allowed_keys = [ + "file_upload_uri", + "fedora_uri", + "source_uri", + "destination_uri", + "args", + "mimetype", + "source_field", + ]; + $keys_to_unset = array_diff(array_keys($data), $allowed_keys); + foreach ($keys_to_unset as $key) { + unset($data[$key]); + } if (!empty($data)) { $event["attachment"] = [ @@ -192,6 +203,7 @@ class EventGenerator implements EventGeneratorInterface { protected function getRevisionIds(Media $media, EntityStorageInterface $media_storage) { $result = $media_storage->getQuery() ->allRevisions() + ->accessCheck(TRUE) ->condition($media->getEntityType()->getKey('id'), $media->id()) ->sort($media->getEntityType()->getKey('revision'), 'DESC') ->execute(); diff --git a/src/EventSubscriber/LinkHeaderSubscriber.php b/src/EventSubscriber/LinkHeaderSubscriber.php index ce33ce2e..f7e5725b 100644 --- a/src/EventSubscriber/LinkHeaderSubscriber.php +++ b/src/EventSubscriber/LinkHeaderSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\islandora\EventSubscriber; +use Symfony\Component\HttpKernel\Event\ResponseEvent; use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; @@ -13,7 +14,6 @@ use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; /** @@ -312,9 +312,9 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { /** * Adds resource-specific link headers to appropriate responses. * - * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event * Event containing the response. */ - abstract public function onResponse(FilterResponseEvent $event); + abstract public function onResponse(ResponseEvent $event); } diff --git a/src/EventSubscriber/MediaLinkHeaderSubscriber.php b/src/EventSubscriber/MediaLinkHeaderSubscriber.php index 3cebbbaa..0f406cf5 100644 --- a/src/EventSubscriber/MediaLinkHeaderSubscriber.php +++ b/src/EventSubscriber/MediaLinkHeaderSubscriber.php @@ -2,10 +2,10 @@ namespace Drupal\islandora\EventSubscriber; +use Symfony\Component\HttpKernel\Event\ResponseEvent; use Drupal\Core\Url; use Drupal\media\MediaInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; /** * Subscribes to MediaLinkHeader Event. @@ -17,7 +17,7 @@ class MediaLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSub /** * {@inheritdoc} */ - public function onResponse(FilterResponseEvent $event) { + public function onResponse(ResponseEvent $event) { $response = $event->getResponse(); $media = $this->getObject($response, 'media'); diff --git a/src/EventSubscriber/NodeLinkHeaderSubscriber.php b/src/EventSubscriber/NodeLinkHeaderSubscriber.php index e00533f7..c4cdaea8 100644 --- a/src/EventSubscriber/NodeLinkHeaderSubscriber.php +++ b/src/EventSubscriber/NodeLinkHeaderSubscriber.php @@ -2,9 +2,9 @@ namespace Drupal\islandora\EventSubscriber; +use Symfony\Component\HttpKernel\Event\ResponseEvent; use Drupal\node\NodeInterface; use Drupal\islandora\IslandoraUtils; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -17,10 +17,10 @@ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubs /** * Adds node-specific link headers to appropriate responses. * - * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event * Event containing the response. */ - public function onResponse(FilterResponseEvent $event) { + public function onResponse(ResponseEvent $event) { $response = $event->getResponse(); $node = $this->getObject($response, 'node'); 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 @@ +fedora = $fedora; $this->mimeTypeGuesser = $mime_type_guesser; + $this->logger = $logger; } /** @@ -143,14 +159,8 @@ class FedoraAdapter implements AdapterInterface { // NonRDFSource's are considered files. Everything else is a // directory. $type = 'dir'; - // phpcs:disable - if (class_exists(\GuzzleHttp\Psr7\Header::class)) { - $links = \GuzzleHttp\Psr7\Header::parse($response->getHeader('Link')); - } - else { - $links = \GuzzleHttp\Psr7\parse_header($response->getHeader('Link')); - } - // phpcs:enable + $links = Header::parse($response->getHeader('Link')); + foreach ($links as $link) { if ($link['rel'] == 'type' && $link[0] == '') { $type = 'file'; @@ -259,7 +269,7 @@ class FedoraAdapter implements AdapterInterface { */ public function write($path, $contents, Config $config) { $headers = [ - 'Content-Type' => $this->mimeTypeGuesser->guess($path), + 'Content-Type' => $this->mimeTypeGuesser->guessMimeType($path), ]; if ($this->has($path)) { $fedora_url = $path; @@ -274,17 +284,17 @@ class FedoraAdapter implements AdapterInterface { $headers ); if (isset($response) && $response->getStatusCode() == 201) { - \Drupal::logger('fedora_flysystem')->info('Created a version in Fedora for ' . $fedora_url); + $this->logger->info('Created a version in Fedora for ' . $fedora_url); } else { - \Drupal::logger('fedora_flysystem')->error( + $this->logger->error( "Client error: `Failed to create a Fedora version of $fedora_url`. Response is " . print_r($response, TRUE) ); } } catch (\Exception $e) { - \Drupal::logger('fedora_flysystem')->error('Caught exception when creating version: ' . $e->getMessage() . "\n"); + $this->logger->error('Caught exception when creating version: ' . $e->getMessage() . "\n"); } } @@ -386,14 +396,7 @@ class FedoraAdapter implements AdapterInterface { $return = NULL; if ($response->getStatusCode() == 410) { $return = FALSE; - // phpcs:disable - if (class_exists(\GuzzleHttp\Psr7\Header::class)) { - $link_headers = \GuzzleHttp\Psr7\Header::parse($response->getHeader('Link')); - } - else { - $link_headers = \GuzzleHttp\Psr7\parse_header($response->getHeader('Link')); - } - // phpcs:enable + $link_headers = Header::parse($response->getHeader('Link')); if ($link_headers) { $tombstones = array_filter($link_headers, function ($o) { return (isset($o['rel']) && $o['rel'] == 'hasTombstone'); diff --git a/src/Flysystem/Fedora.php b/src/Flysystem/Fedora.php index fe7af7ba..0cbf1a12 100644 --- a/src/Flysystem/Fedora.php +++ b/src/Flysystem/Fedora.php @@ -3,6 +3,7 @@ namespace Drupal\islandora\Flysystem; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Url; @@ -12,12 +13,11 @@ use Drupal\islandora\Flysystem\Adapter\FedoraAdapter; use Drupal\jwt\Authentication\Provider\JwtAuth; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\HandlerStack; -use GuzzleHttp\Client; -use Islandora\Chullo\IFedoraApi; use Islandora\Chullo\FedoraApi; +use Islandora\Chullo\IFedoraApi; use Psr\Http\Message\RequestInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; +use Symfony\Component\Mime\MimeTypeGuesserInterface; /** * Drupal plugin for the Fedora Flysystem adapter. @@ -38,7 +38,7 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac /** * Mimetype guesser. * - * @var \Symfony\Component\HttpFoundation\File\Mimetype\MimeTypeGuesserInterface + * @var \Symfony\Component\Mime\MimeTypeGuesserInterface */ protected $mimeTypeGuesser; @@ -49,24 +49,35 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac */ protected $languageManager; + /** + * Logger. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + /** * Constructs a Fedora plugin for Flysystem. * * @param \Islandora\Chullo\IFedoraApi $fedora * Fedora client. - * @param \Symfony\Component\HttpFoundation\File\Mimetype\MimeTypeGuesserInterface $mime_type_guesser + * @param \Symfony\Component\Mime\MimeTypeGuesserInterface $mime_type_guesser * Mimetype guesser. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * Language manager. + * @param \Drupal\Core\Logger\LoggerChannelInterface $logger + * The fedora adapter logger channel. */ public function __construct( IFedoraApi $fedora, MimeTypeGuesserInterface $mime_type_guesser, - LanguageManagerInterface $language_manager + LanguageManagerInterface $language_manager, + LoggerChannelInterface $logger ) { $this->fedora = $fedora; $this->mimeTypeGuesser = $mime_type_guesser; $this->languageManager = $language_manager; + $this->logger = $logger; } /** @@ -77,17 +88,14 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac // Construct guzzle client to middleware that adds JWT. $stack = HandlerStack::create(); $stack->push(static::addJwt($container->get('jwt.authentication.jwt'))); - $client = new Client([ - 'handler' => $stack, - 'base_uri' => $configuration['root'], - ]); - $fedora = new FedoraApi($client); + $fedora = FedoraApi::createWithHandler($configuration['root'], $stack); // Return it. return new static( $fedora, $container->get('file.mime_type.guesser'), - $container->get('language_manager') + $container->get('language_manager'), + $container->get('logger.channel.fedora_flysystem') ); } @@ -116,7 +124,7 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac * {@inheritdoc} */ public function getAdapter() { - return new FedoraAdapter($this->fedora, $this->mimeTypeGuesser); + return new FedoraAdapter($this->fedora, $this->mimeTypeGuesser, $this->logger); } /** diff --git a/src/Form/AddChildrenForm.php b/src/Form/AddChildrenForm.php index 0ff72496..528b4283 100644 --- a/src/Form/AddChildrenForm.php +++ b/src/Form/AddChildrenForm.php @@ -229,7 +229,7 @@ class AddChildrenForm extends AddMediaForm { * @param \Drupal\Core\Routing\RouteMatch $route_match * The current routing match. * - * @return \Drupal\Core\Access\AccessResultAllowed|\Drupal\Core\Access\AccessResultForbidden + * @return \Drupal\Core\Access\AccessResultInterface * Whether we can or can't show the "thing". */ public function access(RouteMatch $route_match) { diff --git a/src/Form/AddChildrenWizard/AbstractBatchProcessor.php b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php new file mode 100644 index 00000000..6193c0c3 --- /dev/null +++ b/src/Form/AddChildrenWizard/AbstractBatchProcessor.php @@ -0,0 +1,258 @@ +entityTypeManager = $entity_type_manager; + $this->database = $database; + $this->currentUser = $current_user; + $this->messenger = $messenger; + $this->dateFormatter = $date_formatter; + } + + /** + * Implements callback_batch_operation() for our child addition batch. + */ + public function batchOperation($delta, $info, array $values, &$context) { + $transaction = $this->database->startTransaction(); + + try { + $entities[] = $node = $this->getNode($info, $values); + $entities[] = $this->createMedia($node, $info, $values); + + $context['results'] = array_merge_recursive($context['results'], [ + 'validation_violations' => $this->validationClassification($entities), + ]); + $context['results']['count'] = ($context['results']['count'] ?? 0) + 1; + } + catch (HttpExceptionInterface $e) { + $transaction->rollBack(); + throw $e; + } + catch (\Exception $e) { + $transaction->rollBack(); + throw new HttpException(500, $e->getMessage(), $e); + } + } + + /** + * Loads the file indicated. + * + * @param mixed $info + * Widget values. + * + * @return \Drupal\file\FileInterface|null + * The loaded file. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getFile($info) : ?FileInterface { + return (is_array($info) && isset($info['target_id'])) ? + $this->entityTypeManager->getStorage('file')->load($info['target_id']) : + NULL; + } + + /** + * Get the node to which to attach our media. + * + * @param mixed $info + * Info from the widget used to create the request. + * @param array $values + * Additional form inputs. + * + * @return \Drupal\node\NodeInterface + * The node to which to attach the created media. + */ + abstract protected function getNode($info, array $values) : NodeInterface; + + /** + * Get a name to use for bulk-created assets. + * + * @param mixed $info + * Widget values. + * @param array $values + * Form values. + * + * @return string + * An applicable name. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getName($info, array $values) : string { + $file = $this->getFile($info); + return $file ? $file->getFilename() : strtr('Bulk ingest, {date}', [ + '{date}' => $this->dateFormatter->format(time(), 'long'), + ]); + } + + /** + * Create a media referencing the given file, associated with the given node. + * + * @param \Drupal\node\NodeInterface $node + * The node to which the media should be associated. + * @param mixed $info + * The widget info for the media source field. + * @param array $values + * Values from the wizard, which should contain at least: + * - media_type: The machine name/ID of the media type as which to create + * the media + * - use: An array of the selected "media use" terms. + * + * @return \Drupal\media\MediaInterface + * The created media entity. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function createMedia(NodeInterface $node, $info, array $values) : MediaInterface { + $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); + + // Create a media with the file attached and also pointing at the node. + $field = $this->getField($values); + + $media_values = array_merge( + [ + 'bundle' => $values['media_type'], + 'name' => $this->getName($info, $values), + IslandoraUtils::MEDIA_OF_FIELD => $node, + IslandoraUtils::MEDIA_USAGE_FIELD => ($values['use'] ? + $taxonomy_term_storage->loadMultiple($values['use']) : + NULL), + 'uid' => $this->currentUser->id(), + // XXX: Published... no constant? + 'status' => 1, + ], + [ + $field->getName() => [ + $info, + ], + ] + ); + $media = $this->entityTypeManager->getStorage('media')->create($media_values); + if ($media->save() !== SAVED_NEW) { + throw new \Exception("Failed to create media."); + } + + return $media; + } + + /** + * Helper to bulk process validatable entities. + * + * @param array $entities + * An array of entities to scan for validation violations. + * + * @return array + * An associative array mapping entity type IDs to entity IDs to a count + * of validation violations found on then given entity. + */ + protected function validationClassification(array $entities) { + $violations = []; + + foreach ($entities as $entity) { + $entity_violations = $entity->validate(); + if ($entity_violations->count() > 0) { + $violations[$entity->getEntityTypeId()][$entity->id()] = $entity_violations->count(); + } + } + + return $violations; + } + + /** + * Implements callback_batch_finished() for our child addition batch. + */ + public function batchProcessFinished($success, $results, $operations): void { + if ($success) { + foreach ($results['validation_violations'] ?? [] as $entity_type => $info) { + foreach ($info as $id => $count) { + $this->messenger->addWarning($this->formatPlural( + $count, + '1 validation error present in bulk created entity of type %type, with ID %id.', + '@count validation errors present in bulk created entity of type %type, with ID %id.', + [ + '%type' => $entity_type, + ':uri' => Url::fromRoute("entity.{$entity_type}.canonical", [$entity_type => $id])->toString(), + '%id' => $id, + ] + )); + } + } + } + else { + $this->messenger->addError($this->t('Encountered an error when processing.')); + } + } + +} diff --git a/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php new file mode 100644 index 00000000..6aeed879 --- /dev/null +++ b/src/Form/AddChildrenWizard/AbstractFileSelectionForm.php @@ -0,0 +1,157 @@ +entityTypeManager = $container->get('entity_type.manager'); + $instance->widgetPluginManager = $container->get('plugin.manager.field.widget'); + $instance->entityFieldManager = $container->get('entity_field.manager'); + $instance->currentUser = $container->get('current_user'); + + $instance->batchProcessor = $container->get(static::BATCH_PROCESSOR); + + return $instance; + } + + /** + * Helper; get the media type, based off discovering from form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return \Drupal\media\MediaTypeInterface + * The target media type. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getMediaTypeFromFormState(FormStateInterface $form_state): MediaTypeInterface { + return $this->getMediaType($form_state->getTemporaryValue('wizard')); + } + + /** + * Helper; get field instance, based off discovering from form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface + * The field definition. + */ + protected function getFieldFromFormState(FormStateInterface $form_state): FieldDefinitionInterface { + $cached_values = $form_state->getTemporaryValue('wizard'); + + $field = $this->getField($cached_values); + $def = $field->getFieldStorageDefinition(); + if ($def instanceof FieldStorageConfigInterface) { + $def->set('cardinality', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + } + elseif ($def instanceof BaseFieldDefinition) { + $def->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + } + else { + throw new \Exception('Unable to remove cardinality limit.'); + } + + return $field; + } + + /** + * Helper; get widget for the field, based on discovering from form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return \Drupal\Core\Field\WidgetInterface + * The widget. + */ + protected function getWidgetFromFormState(FormStateInterface $form_state): WidgetInterface { + return $this->getWidget($this->getFieldFromFormState($form_state)); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + // Using the media type selected in the previous step, grab the + // media bundle's "source" field, and create a multi-file upload widget + // for it, with the same kind of constraints. + $field = $this->getFieldFromFormState($form_state); + $items = FieldItemList::createInstance($field, $field->getName(), $this->getMediaTypeFromFormState($form_state)->getTypedData()); + + $form['#tree'] = TRUE; + $form['#parents'] = []; + $widget = $this->getWidgetFromFormState($form_state); + $form['files'] = $widget->form( + $items, + $form, + $form_state + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + $widget = $this->getWidgetFromFormState($form_state); + $builder = (new BatchBuilder()) + ->setTitle($this->t('Bulk creating...')) + ->setInitMessage($this->t('Initializing...')) + ->setFinishCallback([$this->batchProcessor, 'batchProcessFinished']); + $values = $form_state->getValue($this->getField($cached_values)->getName()); + $massaged_values = $widget->massageFormValues($values, $form, $form_state); + foreach ($massaged_values as $delta => $info) { + $builder->addOperation( + [$this->batchProcessor, 'batchOperation'], + [$delta, $info, $cached_values] + ); + } + batch_set($builder->toArray()); + } + +} diff --git a/src/Form/AddChildrenWizard/AbstractForm.php b/src/Form/AddChildrenWizard/AbstractForm.php new file mode 100644 index 00000000..e9fac387 --- /dev/null +++ b/src/Form/AddChildrenWizard/AbstractForm.php @@ -0,0 +1,125 @@ +nodeId = $this->routeMatch->getParameter('node'); + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function getParameters() : array { + return array_merge( + parent::getParameters(), + [ + 'tempstore_id' => static::TEMPSTORE_ID, + 'current_user' => \Drupal::service('current_user'), + ] + ); + } + + /** + * {@inheritdoc} + */ + public function getOperations($cached_values) { + $ops = []; + + $ops['type_selection'] = [ + 'title' => $this->t('Type Selection'), + 'form' => static::TYPE_SELECTION_FORM, + 'values' => [ + 'node' => $this->nodeId, + ], + ]; + $ops['file_selection'] = [ + 'title' => $this->t('Widget Input for Selected Type'), + 'form' => static::FILE_SELECTION_FORM, + 'values' => [ + 'node' => $this->nodeId, + ], + ]; + + return $ops; + } + + /** + * {@inheritdoc} + */ + public function getNextParameters($cached_values) { + return parent::getNextParameters($cached_values) + ['node' => $this->nodeId]; + } + + /** + * {@inheritdoc} + */ + public function getPreviousParameters($cached_values) { + return parent::getPreviousParameters($cached_values) + ['node' => $this->nodeId]; + } + +} diff --git a/src/Form/AddChildrenWizard/Access.php b/src/Form/AddChildrenWizard/Access.php new file mode 100644 index 00000000..0adafde5 --- /dev/null +++ b/src/Form/AddChildrenWizard/Access.php @@ -0,0 +1,71 @@ +utils = $utils; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) : self { + return new static( + $container->get('islandora.utils') + ); + } + + /** + * Check if the user can create any "Islandora" nodes and media. + * + * @param \Drupal\Core\Routing\RouteMatch $route_match + * The current routing match. + * + * @return \Drupal\Core\Access\AccessResultInterface + * Whether we can or cannot show the "thing". + */ + public function childAccess(RouteMatch $route_match) : AccessResultInterface { + return AccessResult::allowedIf($this->utils->canCreateIslandoraEntity('node', 'node_type')) + ->andIf($this->mediaAccess($route_match)); + + } + + /** + * Check if the user can create any "Islandora" media. + * + * @param \Drupal\Core\Routing\RouteMatch $route_match + * The current routing match. + * + * @return \Drupal\Core\Access\AccessResultInterface + * Whether we can or cannot show the "thing". + */ + public function mediaAccess(RouteMatch $route_match) : AccessResultInterface { + return AccessResult::allowedIf($this->utils->canCreateIslandoraEntity('media', 'media_type')); + } + +} diff --git a/src/Form/AddChildrenWizard/ChildBatchProcessor.php b/src/Form/AddChildrenWizard/ChildBatchProcessor.php new file mode 100644 index 00000000..084e7816 --- /dev/null +++ b/src/Form/AddChildrenWizard/ChildBatchProcessor.php @@ -0,0 +1,57 @@ +entityTypeManager->getStorage('taxonomy_term'); + $node_storage = $this->entityTypeManager->getStorage('node'); + $parent = $node_storage->load($values['node']); + + // Create a node (with the filename?) (and also belonging to the target + // node). + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->create([ + 'type' => $values['bundle'], + 'title' => $this->getName($info, $values), + IslandoraUtils::MEMBER_OF_FIELD => $parent, + 'uid' => $this->currentUser->id(), + 'status' => NodeInterface::PUBLISHED, + IslandoraUtils::MODEL_FIELD => ($values['model'] ? + $taxonomy_term_storage->load($values['model']) : + NULL), + ]); + + if ($node->save() !== SAVED_NEW) { + throw new \Exception("Failed to create node."); + } + + return $node; + } + + /** + * {@inheritdoc} + */ + public function batchProcessFinished($success, $results, $operations): void { + if ($success) { + $this->messenger->addMessage($this->formatPlural( + $results['count'], + 'Added 1 child node.', + 'Added @count child nodes.' + )); + } + + parent::batchProcessFinished($success, $results, $operations); + } + +} diff --git a/src/Form/AddChildrenWizard/ChildFileSelectionForm.php b/src/Form/AddChildrenWizard/ChildFileSelectionForm.php new file mode 100644 index 00000000..9783d082 --- /dev/null +++ b/src/Form/AddChildrenWizard/ChildFileSelectionForm.php @@ -0,0 +1,32 @@ +getTemporaryValue('wizard'); + $form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members")); + } + +} diff --git a/src/Form/AddChildrenWizard/ChildForm.php b/src/Form/AddChildrenWizard/ChildForm.php new file mode 100644 index 00000000..0b9a197f --- /dev/null +++ b/src/Form/AddChildrenWizard/ChildForm.php @@ -0,0 +1,24 @@ + $this->currentUser->id(), + '{nodeid}' => $this->nodeId, + ]); + } + +} diff --git a/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php b/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php new file mode 100644 index 00000000..f5795997 --- /dev/null +++ b/src/Form/AddChildrenWizard/ChildTypeSelectionForm.php @@ -0,0 +1,157 @@ +nodeBundleOptions === NULL) { + $this->nodeBundleOptions = []; + $this->nodeBundleHasModelField = []; + + $access_handler = $this->entityTypeManager->getAccessControlHandler('node'); + foreach ($this->entityTypeBundleInfo->getBundleInfo('node') as $bundle => $info) { + $access = $access_handler->createAccess( + $bundle, + NULL, + [], + TRUE + ); + $this->cacheableMetadata->addCacheableDependency($access); + if (!$access->isAllowed()) { + continue; + } + $this->nodeBundleOptions[$bundle] = $info['label']; + $fields = $this->entityFieldManager->getFieldDefinitions('node', $bundle); + $this->nodeBundleHasModelField[$bundle] = array_key_exists(IslandoraUtils::MODEL_FIELD, $fields); + } + } + + return $this->nodeBundleOptions; + } + + /** + * Generates a mapping of taxonomy term IDs to their names. + * + * @return \Generator + * The mapping of taxonomy term IDs to their names. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getModelOptions() : \Generator { + $terms = $this->entityTypeManager->getStorage('taxonomy_term') + ->loadTree('islandora_models', 0, NULL, TRUE); + foreach ($terms as $term) { + yield $term->id() => $term->getName(); + } + } + + /** + * Helper; map node bundles supporting the "has model" field, for #states. + * + * @return \Generator + * Yields associative array mapping the string 'value' to the bundles which + * have the given field. + */ + protected function mapModelStates() : \Generator { + $this->getNodeBundleOptions(); + foreach (array_keys(array_filter($this->nodeBundleHasModelField)) as $bundle) { + yield ['value' => $bundle]; + } + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $this->cacheableMetadata = CacheableMetadata::createFromRenderArray($form) + ->addCacheContexts([ + 'url', + 'url.query_args', + ]); + $cached_values = $form_state->getTemporaryValue('wizard'); + + $form['bundle'] = [ + '#type' => 'select', + '#title' => $this->t('Content Type'), + '#description' => $this->t('Each child created will have this content type.'), + '#empty_value' => '', + '#default_value' => $cached_values['bundle'] ?? '', + '#options' => $this->getNodeBundleOptions(), + '#required' => TRUE, + ]; + + $model_states = iterator_to_array($this->mapModelStates()); + $form['model'] = [ + '#type' => 'select', + '#title' => $this->t('Model'), + '#description' => $this->t('Each child will be tagged with this model.'), + '#options' => iterator_to_array($this->getModelOptions()), + '#empty_value' => '', + '#default_value' => $cached_values['model'] ?? '', + '#states' => [ + 'visible' => [ + ':input[name="bundle"]' => $model_states, + ], + 'required' => [ + ':input[name="bundle"]' => $model_states, + ], + ], + ]; + + $this->cacheableMetadata->applyTo($form); + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected static function keysToSave() : array { + return array_merge( + parent::keysToSave(), + [ + 'bundle', + 'model', + ] + ); + } + +} diff --git a/src/Form/AddChildrenWizard/FieldTrait.php b/src/Form/AddChildrenWizard/FieldTrait.php new file mode 100644 index 00000000..830f95cd --- /dev/null +++ b/src/Form/AddChildrenWizard/FieldTrait.php @@ -0,0 +1,66 @@ +getMediaType($values); + $media_source = $media_type->getSource(); + $source_field = $media_source->getSourceFieldDefinition($media_type); + + $fields = $this->entityFieldManager()->getFieldDefinitions('media', $media_type->id()); + + return $fields[$source_field->getFieldStorageDefinition()->getName()] ?? + $media_source->createSourceField($media_type); + } + + /** + * Lazy-initialization of the entity field manager service. + * + * @return \Drupal\Core\Entity\EntityFieldManagerInterface + * The entity field manager service. + */ + protected function entityFieldManager() : EntityFieldManagerInterface { + if ($this->entityFieldManager === NULL) { + $this->setEntityFieldManager(\Drupal::service('entity_field.manager')); + } + return $this->entityFieldManager; + } + + /** + * Setter for entity field manager. + */ + public function setEntityFieldManager(EntityFieldManagerInterface $entity_field_manager) : self { + $this->entityFieldManager = $entity_field_manager; + return $this; + } + +} diff --git a/src/Form/AddChildrenWizard/MediaBatchProcessor.php b/src/Form/AddChildrenWizard/MediaBatchProcessor.php new file mode 100644 index 00000000..9a54f03b --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaBatchProcessor.php @@ -0,0 +1,34 @@ +entityTypeManager->getStorage('node')->load($values['node']); + } + + /** + * {@inheritdoc} + */ + public function batchProcessFinished($success, $results, $operations): void { + if ($success) { + $this->messenger->addMessage($this->formatPlural( + $results['count'], + 'Added 1 media.', + 'Added @count media.' + )); + } + + parent::batchProcessFinished($success, $results, $operations); + } + +} diff --git a/src/Form/AddChildrenWizard/MediaFileSelectionForm.php b/src/Form/AddChildrenWizard/MediaFileSelectionForm.php new file mode 100644 index 00000000..534c7309 --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaFileSelectionForm.php @@ -0,0 +1,32 @@ +getTemporaryValue('wizard'); + $form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/media")); + } + +} diff --git a/src/Form/AddChildrenWizard/MediaForm.php b/src/Form/AddChildrenWizard/MediaForm.php new file mode 100644 index 00000000..2e6fa217 --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaForm.php @@ -0,0 +1,24 @@ + $this->currentUser->id(), + '{nodeid}' => $this->nodeId, + ]); + } + +} diff --git a/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php b/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php new file mode 100644 index 00000000..b06d004d --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaTypeSelectionForm.php @@ -0,0 +1,227 @@ +entityTypeBundleInfo = $container->get('entity_type.bundle.info'); + $instance->entityTypeManager = $container->get('entity_type.manager'); + $instance->entityFieldManager = $container->get('entity_field.manager'); + $instance->utils = $container->get('islandora.utils'); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getFormId() : string { + return 'islandora_add_media_type_selection'; + } + + /** + * Memoization for ::getMediaBundleOptions(). + * + * @var array|null + */ + protected ?array $mediaBundleOptions = NULL; + + /** + * Indicate presence of usage field on media bundles. + * + * Populated as a side effect in ::getMediaBundleOptions(). + * + * @var array|null + */ + protected ?array $mediaBundleUsageField = NULL; + + /** + * Helper; get options for media types. + * + * @return array + * An associative array mapping the machine name of the media type to its + * human-readable label. + */ + protected function getMediaBundleOptions() : array { + if ($this->mediaBundleOptions === NULL) { + $this->mediaBundleOptions = []; + $this->mediaBundleUsageField = []; + + $access_handler = $this->entityTypeManager->getAccessControlHandler('media'); + foreach ($this->entityTypeBundleInfo->getBundleInfo('media') as $bundle => $info) { + if (!$this->utils->isIslandoraType('media', $bundle)) { + continue; + } + $access = $access_handler->createAccess( + $bundle, + NULL, + [], + TRUE + ); + $this->cacheableMetadata->addCacheableDependency($access); + if (!$access->isAllowed()) { + continue; + } + $this->mediaBundleOptions[$bundle] = $info['label']; + $fields = $this->entityFieldManager->getFieldDefinitions('media', $bundle); + $this->mediaBundleUsageField[$bundle] = array_key_exists(IslandoraUtils::MEDIA_USAGE_FIELD, $fields); + } + } + + return $this->mediaBundleOptions; + } + + /** + * Helper; list the terms of the "islandora_media_use" vocabulary. + * + * @return \Generator + * Generates term IDs as keys mapping to term names. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getMediaUseOptions() : \Generator { + /** @var \Drupal\taxonomy\TermInterface[] $terms */ + $terms = $this->entityTypeManager->getStorage('taxonomy_term') + ->loadTree('islandora_media_use', 0, NULL, TRUE); + + foreach ($terms as $term) { + yield $term->id() => $term->getName(); + } + } + + /** + * Helper; map media types supporting the usage field for use with #states. + * + * @return \Generator + * Yields associative array mapping the string 'value' to the bundles which + * have the given field. + */ + protected function mapUseStates(): \Generator { + $this->getMediaBundleOptions(); + foreach (array_keys(array_filter($this->mediaBundleUsageField)) as $bundle) { + yield ['value' => $bundle]; + } + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $this->cacheableMetadata = CacheableMetadata::createFromRenderArray($form) + ->addCacheContexts([ + 'url', + 'url.query_args', + ]); + $cached_values = $form_state->getTemporaryValue('wizard'); + + $form['media_type'] = [ + '#type' => 'select', + '#title' => $this->t('Media Type'), + '#description' => $this->t('Each media created will have this type.'), + '#empty_value' => '', + '#default_value' => $cached_values['media_type'] ?? '', + '#options' => $this->getMediaBundleOptions(), + '#required' => TRUE, + ]; + $use_states = iterator_to_array($this->mapUseStates()); + $form['use'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Usage'), + '#description' => $this->t('Defined by Portland Common Data Model: Use Extension. "Original File" will trigger creation of derivatives.', [ + ':url' => 'https://pcdm.org/2015/05/12/use', + ]), + '#options' => iterator_to_array($this->getMediaUseOptions()), + '#default_value' => $cached_values['use'] ?? [], + '#states' => [ + 'visible' => [ + ':input[name="media_type"]' => $use_states, + ], + 'required' => [ + ':input[name="media_type"]' => $use_states, + ], + ], + ]; + + $this->cacheableMetadata->applyTo($form); + return $form; + } + + /** + * Helper; enumerate keys to persist in form state. + * + * @return string[] + * The keys to be persisted in our temp value in form state. + */ + protected static function keysToSave() : array { + return [ + 'media_type', + 'use', + ]; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + foreach (static::keysToSave() as $key) { + $cached_values[$key] = $form_state->getValue($key); + } + $form_state->setTemporaryValue('wizard', $cached_values); + } + +} diff --git a/src/Form/AddChildrenWizard/MediaTypeTrait.php b/src/Form/AddChildrenWizard/MediaTypeTrait.php new file mode 100644 index 00000000..36cf6ff2 --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaTypeTrait.php @@ -0,0 +1,58 @@ +entityTypeManager()->getStorage('media_type')->load($values['media_type']); + } + + /** + * Lazy-initialization of the entity type manager service. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager service. + */ + protected function entityTypeManager() : EntityTypeManagerInterface { + if ($this->entityTypeManager === NULL) { + $this->setEntityTypeManager(\Drupal::service('entity_type.manager')); + } + return $this->entityTypeManager; + } + + /** + * Setter for the entity type manager service. + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) : self { + $this->entityTypeManager = $entity_type_manager; + return $this; + } + +} diff --git a/src/Form/AddChildrenWizard/WizardTrait.php b/src/Form/AddChildrenWizard/WizardTrait.php new file mode 100644 index 00000000..dd56450f --- /dev/null +++ b/src/Form/AddChildrenWizard/WizardTrait.php @@ -0,0 +1,40 @@ +widgetPluginManager->getInstance([ + 'field_definition' => $field, + 'form_mode' => 'default', + 'prepare' => TRUE, + ]); + } + +} diff --git a/src/Form/ConfirmDeleteMediaAndFile.php b/src/Form/ConfirmDeleteMediaAndFile.php index 64c1bff3..a9613871 100644 --- a/src/Form/ConfirmDeleteMediaAndFile.php +++ b/src/Form/ConfirmDeleteMediaAndFile.php @@ -128,6 +128,9 @@ class ConfirmDeleteMediaAndFile extends DeleteMultipleForm { // Check for files. $fields = $this->entityFieldManager->getFieldDefinitions('media', $entity->bundle()); foreach ($fields as $field) { + if ($field->getName() == 'thumbnail') { + continue; + } $type = $field->getType(); if ($type == 'file' || $type == 'image') { $target_id = $entity->get($field->getName())->target_id; @@ -137,8 +140,11 @@ class ConfirmDeleteMediaAndFile extends DeleteMultipleForm { $inaccessible_entities[] = $file; continue; } - $delete_files[$file->id()] = $file; - $total_count++; + if (!array_key_exists($file->id(), $delete_files)) { + $delete_files[$file->id()] = $file; + $total_count++; + } + } } } diff --git a/src/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php index 0ebc0b3c..ad4c83b3 100644 --- a/src/Form/IslandoraSettingsForm.php +++ b/src/Form/IslandoraSettingsForm.php @@ -2,8 +2,10 @@ namespace Drupal\islandora\Form; +use Drupal\Core\Cache\Cache; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Site\Settings; @@ -39,6 +41,9 @@ class IslandoraSettingsForm extends ConfigFormBase { 'month', 'year', ]; + const GEMINI_PSEUDO_FIELD = 'field_gemini_uri'; + const NODE_DELETE_MEDIA_AND_FILES = 'delete_media_and_files'; + const REDIRECT_AFTER_MEDIA_SAVE = 'redirect_after_media_save'; /** * To list the available bundle types. @@ -54,6 +59,13 @@ class IslandoraSettingsForm extends ConfigFormBase { */ private $brokerPassword; + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + private $entityTypeManager; + /** * Constructs a \Drupal\system\ConfigFormBase object. * @@ -61,14 +73,18 @@ class IslandoraSettingsForm extends ConfigFormBase { * The factory for configuration objects. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info * The EntityTypeBundleInfo service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The EntityTypeManager service. */ public function __construct( ConfigFactoryInterface $config_factory, - EntityTypeBundleInfoInterface $entity_type_bundle_info + EntityTypeBundleInfoInterface $entity_type_bundle_info, + EntityTypeManagerInterface $entity_type_manager ) { $this->setConfigFactory($config_factory); $this->entityTypeBundleInfo = $entity_type_bundle_info; $this->brokerPassword = $this->config(self::CONFIG_NAME)->get(self::BROKER_PASSWORD); + $this->entityTypeManager = $entity_type_manager; } /** @@ -78,6 +94,7 @@ class IslandoraSettingsForm extends ConfigFormBase { return new static( $container->get('config.factory'), $container->get('entity_type.bundle.info'), + $container->get('entity_type.manager') ); } @@ -112,6 +129,9 @@ class IslandoraSettingsForm extends ConfigFormBase { '#type' => 'textfield', '#title' => $this->t('URL'), '#default_value' => $config->get(self::BROKER_URL), + '#config' => [ + 'key' => 'islandora.settings:' . self::BROKER_URL, + ], ]; $broker_user = $config->get(self::BROKER_USER); $form['broker_info']['provide_user_creds'] = [ @@ -132,6 +152,9 @@ class IslandoraSettingsForm extends ConfigFormBase { $state_selector => ['checked' => TRUE], ], ], + '#config' => [ + 'key' => 'islandora.settings:' . self::BROKER_USER, + ], ]; $form['broker_info'][self::BROKER_PASSWORD] = [ '#type' => 'password', @@ -142,6 +165,10 @@ class IslandoraSettingsForm extends ConfigFormBase { $state_selector => ['checked' => TRUE], ], ], + '#config' => [ + 'key' => 'islandora.settings:' . self::BROKER_PASSWORD, + 'secret' => TRUE, + ], ]; $form[self::JWT_EXPIRY] = [ '#type' => 'textfield', @@ -186,10 +213,29 @@ 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::REDIRECT_AFTER_MEDIA_SAVE] = [ + '#type' => 'checkbox', + '#title' => $this->t('Redirect after media save.'), + '#description' => $this->t('Redirect to node-specific media list after creation of media.'), + '#default_value' => (bool) $config->get(self::REDIRECT_AFTER_MEDIA_SAVE), + ]; + $form[self::FEDORA_URL] = [ '#type' => 'textfield', '#title' => $this->t('Fedora URL'), - '#attributes' => ['readonly' => 'readonly'], + '#description' => $this->t('Read-only. This value is set in settings.php as the URL for the Fedora flysystem.'), + '#attributes' => [ + 'readonly' => 'readonly', + 'disabled' => 'disabled', + ], '#default_value' => $fedora_url, ]; @@ -308,7 +354,7 @@ class IslandoraSettingsForm extends ConfigFormBase { public function submitForm(array &$form, FormStateInterface $form_state) { $config = $this->configFactory->getEditable(self::CONFIG_NAME); - $pseudo_types = array_filter($form_state->getValue(self::GEMINI_PSEUDO)); + $new_pseudo_types = array_filter($form_state->getValue(self::GEMINI_PSEUDO)); $broker_password = $form_state->getValue(self::BROKER_PASSWORD); @@ -326,15 +372,59 @@ class IslandoraSettingsForm extends ConfigFormBase { } } + // Check for types being unset and remove the field from them first. + $current_pseudo_types = $config->get(self::GEMINI_PSEUDO); + $this->updateEntityViewConfiguration($current_pseudo_types, $new_pseudo_types); + $config ->set(self::BROKER_URL, $form_state->getValue(self::BROKER_URL)) ->set(self::JWT_EXPIRY, $form_state->getValue(self::JWT_EXPIRY)) ->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, $pseudo_types) + ->set(self::GEMINI_PSEUDO, $new_pseudo_types) + ->set(self::NODE_DELETE_MEDIA_AND_FILES, $form_state->getValue(self::NODE_DELETE_MEDIA_AND_FILES)) + ->set(self::REDIRECT_AFTER_MEDIA_SAVE, $form_state->getValue(self::REDIRECT_AFTER_MEDIA_SAVE)) ->save(); parent::submitForm($form, $form_state); } + /** + * Removes the Fedora URI field from entity bundles that have be unselected. + * + * @param array $current_config + * The current set of entity types & bundles to have the pseudo field, + * format {bundle}:{entity_type}. + * @param array $new_config + * The new set of entity types & bundles to have the pseudo field, format + * {bundle}:{entity_type}. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\Entity\EntityStorageException + */ + private function updateEntityViewConfiguration(array $current_config, array $new_config) { + $removed = array_diff($current_config, $new_config); + $added = array_diff($new_config, $current_config); + $entity_view_display = $this->entityTypeManager->getStorage('entity_view_display'); + foreach ($removed as $bundle_type) { + [$bundle, $type_id] = explode(":", $bundle_type); + $results = $entity_view_display->getQuery() + ->condition('bundle', $bundle) + ->condition('targetEntityType', $type_id) + ->exists('content.' . self::GEMINI_PSEUDO_FIELD . '.region') + ->execute(); + $entities = $entity_view_display->loadMultiple($results); + foreach ($entities as $entity) { + $entity->removeComponent(self::GEMINI_PSEUDO_FIELD); + $entity->save(); + } + } + if (count($removed) > 0 || count($added) > 0) { + // If we added or cleared a type then clear the extra_fields cache. + // @see Drupal/Core/Entity/EntityFieldManager::getExtraFields + Cache::invalidateTags(["entity_field_info"]); + } + } + } diff --git a/src/IslandoraContextManager.php b/src/IslandoraContextManager.php index 801e3253..9fd93fbc 100644 --- a/src/IslandoraContextManager.php +++ b/src/IslandoraContextManager.php @@ -13,6 +13,14 @@ use Drupal\Component\Plugin\Exception\ContextException; */ class IslandoraContextManager extends ContextManager { + /** + * Allow the contexts to be reset before evaluation. + */ + protected function resetContextEvaluation() { + $this->contexts = []; + $this->contextConditionsEvaluated = FALSE; + } + /** * Evaluate all context conditions. * @@ -22,7 +30,11 @@ class IslandoraContextManager extends ContextManager { public function evaluateContexts(array $provided = []) { $this->activeContexts = []; - + // XXX: Ensure that no earlier executed contexts in the request are still + // present when being triggered via Islandora's ContextProviders. + if (!empty($provided)) { + $this->resetContextEvaluation(); + } /** @var \Drupal\context\ContextInterface $context */ foreach ($this->getContexts() as $context) { if ($this->evaluateContextConditions($context, $provided) && !$context->disabled()) { @@ -48,7 +60,11 @@ class IslandoraContextManager extends ContextManager { $conditions = $context->getConditions(); // Apply context to any context aware conditions. - $this->applyContexts($conditions, $provided); + // Abort if the application of contexts has been unsuccessful + // similarly to BlockAccessControlHandler::checkAccess(). + if (!$this->applyContexts($conditions, $provided)) { + return FALSE; + } // Set the logic to use when validating the conditions. $logic = $context->requiresAllConditions() @@ -76,6 +92,13 @@ class IslandoraContextManager extends ContextManager { * TRUE if conditions pass */ protected function applyContexts(ConditionPluginCollection &$conditions, array $provided = []) { + + // If no contexts to check, the return should be TRUE. + // For example, empty is the same as sitewide condition. + if (count($conditions) === 0) { + return TRUE; + } + $passed = FALSE; foreach ($conditions as $condition) { if ($condition instanceof ContextAwarePluginInterface) { try { @@ -86,14 +109,15 @@ class IslandoraContextManager extends ContextManager { $contexts = $provided; } $this->contextHandler->applyContextMapping($condition, $contexts); + $passed = TRUE; } catch (ContextException $e) { - return FALSE; + continue; } } } - return TRUE; + return $passed; } } diff --git a/src/IslandoraUtils.php b/src/IslandoraUtils.php index a4dda6c1..a2df7589 100644 --- a/src/IslandoraUtils.php +++ b/src/IslandoraUtils.php @@ -148,6 +148,7 @@ class IslandoraUtils { return []; } $mids = $this->entityTypeManager->getStorage('media')->getQuery() + ->accessCheck(TRUE) ->condition(self::MEDIA_OF_FIELD, $node->id()) ->execute(); if (empty($mids)) { @@ -208,6 +209,7 @@ class IslandoraUtils { // Query for media that reference this file. $query = $this->entityTypeManager->getStorage('media')->getQuery(); + $query->accessCheck(TRUE); $group = $query->orConditionGroup(); foreach ($conditions as $condition) { $group->condition($condition, $fid); @@ -252,6 +254,7 @@ class IslandoraUtils { } $results = $query + ->accessCheck(TRUE) ->condition($orGroup) ->execute(); @@ -498,6 +501,7 @@ class IslandoraUtils { array_walk($node_fields, $remove_entity); $query = $this->entityTypeManager->getStorage('media')->getQuery(); + $query->accessCheck(TRUE); $taxon_condition = $this->getEntityQueryOrCondition($query, $term_fields, $term->id()); $query->condition($taxon_condition); $node_condition = $this->getEntityQueryOrCondition($query, $node_fields, $node->id()); @@ -672,4 +676,83 @@ class IslandoraUtils { return FALSE; } + /** + * Recursively finds ancestors of an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being checked. + * @param array $fields + * An optional array where the values are the field names to be used for + * retrieval. + * @param int|bool $max_height + * How many levels of checking should be done when retrieving ancestors. + * + * @return array + * An array where the keys and values are the node IDs of the ancestors. + */ + public function findAncestors(ContentEntityInterface $entity, array $fields = [self::MEMBER_OF_FIELD], $max_height = FALSE): array { + // XXX: If a negative integer is passed assume it's false. + if ($max_height < 0) { + $max_height = FALSE; + } + $context = [ + 'max_height' => $max_height, + 'ancestors' => [], + ]; + $this->findAncestorsByEntityReference($entity, $context, $fields); + return $context['ancestors']; + } + + /** + * Helper that builds up the ancestors. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being checked. + * @param array $context + * An array containing: + * -ancestors: The ancestors that have been found. + * -max_height: How far up the chain to go. + * @param array $fields + * An optional array where the values are the field names to be used for + * retrieval. + * @param int $current_height + * The current height of the recursion. + */ + protected function findAncestorsByEntityReference(ContentEntityInterface $entity, array &$context, array $fields = [self::MEMBER_OF_FIELD], int $current_height = 1): void { + $parents = $this->getParentsByEntityReference($entity, $fields); + foreach ($parents as $parent) { + if (isset($context['ancestors'][$parent->id()])) { + continue; + } + $context['ancestors'][$parent->id()] = $parent->id(); + if ($context['max_height'] === FALSE || $current_height < $context['max_height']) { + $this->findAncestorsByEntityReference($parent, $context, $fields, $current_height + 1); + } + } + } + + /** + * Helper that gets the immediate parents of a node. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being checked. + * @param array $fields + * An array where the values are the field names to be used. + * + * @return array + * An array of entity objects keyed by field item deltas. + */ + protected function getParentsByEntityReference(ContentEntityInterface $entity, array $fields): array { + $parents = []; + foreach ($fields as $field) { + if ($entity->hasField($field)) { + $reference_field = $entity->get($field); + if (!$reference_field->isEmpty()) { + $parents = array_merge($parents, $reference_field->referencedEntities()); + } + } + } + return $parents; + } + } diff --git a/src/MediaSource/MediaSourceService.php b/src/MediaSource/MediaSourceService.php index 9399e334..53378985 100644 --- a/src/MediaSource/MediaSourceService.php +++ b/src/MediaSource/MediaSourceService.php @@ -3,6 +3,7 @@ namespace Drupal\islandora\MediaSource; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Session\AccountInterface; @@ -113,8 +114,12 @@ class MediaSourceService { * @param \Drupal\media\MediaInterface $media * Media whose source field you are searching for. * - * @return \Drupal\file\FileInterface - * File if it exists + * @return \Drupal\file\FileInterface|\Drupal\Core\Entity\EntityInterface|false|null + * The first source entity if there is one, generally expected to be of + * \Drupal\file\FileInterface. Boolean FALSE if there was no such entity. + * NULL if the source field does not refer to Drupal entities (as in, the + * field is not a \Drupal\Core\Field\EntityReferenceFieldItemListInterface + * implementation). * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ @@ -127,10 +132,13 @@ class MediaSourceService { } // Get the file from the media. - $files = $media->get($source_field)->referencedEntities(); - $file = reset($files); + $source_list = $media->get($source_field); + if ($source_list instanceof EntityReferenceFieldItemListInterface) { + $files = $source_list->referencedEntities(); + return reset($files); + } - return $file; + return NULL; } /** @@ -268,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"); @@ -349,8 +357,8 @@ class MediaSourceService { 'uri' => $content_location, 'filename' => $this->fileSystem->basename($content_location), 'filemime' => $mimetype, - 'status' => FILE_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/AbstractGenerateDerivativeBase.php b/src/Plugin/Action/AbstractGenerateDerivativeBase.php index 3c7ff5b6..9d0520a3 100644 --- a/src/Plugin/Action/AbstractGenerateDerivativeBase.php +++ b/src/Plugin/Action/AbstractGenerateDerivativeBase.php @@ -5,6 +5,7 @@ namespace Drupal\islandora\Plugin\Action; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\islandora\IslandoraUtils; @@ -94,6 +95,8 @@ class AbstractGenerateDerivativeBase extends EmitEvent { * Field Manager service. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * Event dispatcher service. + * @param \Drupal\Core\Logger\LoggerChannelInterface $channel + * The logger channel. */ public function __construct( array $configuration, @@ -109,7 +112,8 @@ class AbstractGenerateDerivativeBase extends EmitEvent { MessengerInterface $messenger, ConfigFactoryInterface $config, EntityFieldManagerInterface $entity_field_manager, - EventDispatcherInterface $event_dispatcher + EventDispatcherInterface $event_dispatcher, + LoggerChannelInterface $channel ) { $this->utils = $utils; $this->mediaSource = $media_source; @@ -126,7 +130,8 @@ class AbstractGenerateDerivativeBase extends EmitEvent { $event_generator, $stomp, $messenger, - $event_dispatcher + $event_dispatcher, + $channel ); } @@ -148,7 +153,8 @@ class AbstractGenerateDerivativeBase extends EmitEvent { $container->get('messenger'), $container->get('config.factory'), $container->get('entity_field.manager'), - $container->get('event_dispatcher') + $container->get('event_dispatcher'), + $container->get('logger.channel.islandora') ); } diff --git a/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php b/src/Plugin/Action/AbstractGenerateDerivativeMediaFile.php index 84c064e9..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", @@ -40,7 +43,7 @@ class AbstractGenerateDerivativeMediaFile extends AbstractGenerateDerivativeBase protected function generateData(EntityInterface $entity) { $data = parent::generateData($entity); if (get_class($entity) != 'Drupal\media\Entity\Media') { - return; + throw new \RuntimeException("Entity {$entity->getEntityTypeId()} {$entity->id()} is not a media", 500); } $source_file = $this->mediaSource->getSourceFile($entity); if (!$source_file) { @@ -88,19 +91,33 @@ class AbstractGenerateDerivativeMediaFile extends AbstractGenerateDerivativeBase */ 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); + + $map = $this->entityFieldManager->getFieldMapByFieldType('image'); + $image_fields = $map['media']; + $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/Condition/NodeHasAncestor.php b/src/Plugin/Condition/NodeHasAncestor.php new file mode 100644 index 00000000..ad6f1b99 --- /dev/null +++ b/src/Plugin/Condition/NodeHasAncestor.php @@ -0,0 +1,171 @@ +entityTypeManager = $entity_type_manager; + $this->utils = $islandora_utils; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('islandora.utils') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration(): array { + return parent::defaultConfiguration() + [ + 'ancestor_nids' => FALSE, + 'parent_reference_field' => IslandoraUtils::MEMBER_OF_FIELD, + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $default_nids = FALSE; + if ($this->configuration['ancestor_nids']) { + $default_nids = array_map(function ($nid) { + return $this->entityTypeManager->getStorage('node')->load($nid); + }, $this->configuration['ancestor_nids']); + } + $form['ancestor_nids'] = [ + '#type' => 'entity_autocomplete', + '#title' => $this->t('Parent node(s)'), + '#default_value' => $default_nids, + '#required' => FALSE, + '#description' => $this->t("Can be a collection node, compound object or paged content. Accepts multiple values separated by a comma."), + '#target_type' => 'node', + '#tags' => TRUE, + ]; + + $options = []; + $reference_fields = $this->entityTypeManager->getStorage('field_storage_config')->loadByProperties([ + 'type' => 'entity_reference', + 'settings' => [ + 'target_type' => 'node', + ], + ]); + foreach ($reference_fields as $field) { + $options[$field->get('field_name')] = $field->get('field_name'); + } + $form['parent_reference_field'] = [ + '#type' => 'select', + '#title' => $this->t('Direct parent reference'), + '#options' => $options, + '#default_value' => $this->configuration['parent_reference_field'], + '#required' => TRUE, + '#description' => $this->t('Field that contains the reference to its parent node.'), + ]; + + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + // Entity autocomplete store things with target IDs, for convenience just + // store the plain nid. + if (!empty($form_state->getValue('ancestor_nids'))) { + $this->configuration['ancestor_nids'] = array_map(function ($nid) { + return $nid['target_id']; + }, $form_state->getValue('ancestor_nids')); + } + $this->configuration['parent_reference_field'] = $form_state->getValue('parent_reference_field'); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + if (empty($this->configuration['ancestor_nids']) && !$this->isNegated()) { + return TRUE; + } + + $node = $this->getContextValue('node'); + if (!$node) { + return FALSE; + } + + $ancestors = $this->utils->findAncestors($node); + return !empty(array_intersect($this->configuration['ancestor_nids'], $ancestors)); + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (!empty($this->configuration['negate'])) { + return $this->t('The node does not have node @nid as one of its ancestors.', ['@nid' => $this->configuration['ancestor_nids']]); + } + else { + return $this->t('The node has node @nid as one of its ancestors.', ['@nid' => $this->configuration['ancestor_nids']]); + } + } + +} diff --git a/src/Plugin/Condition/NodeIsIslandoraObject.php b/src/Plugin/Condition/NodeIsIslandoraObject.php index 6448cfdc..06ff6259 100644 --- a/src/Plugin/Condition/NodeIsIslandoraObject.php +++ b/src/Plugin/Condition/NodeIsIslandoraObject.php @@ -5,13 +5,14 @@ namespace Drupal\islandora\Plugin\Condition; use Drupal\Core\Condition\ConditionPluginBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Islandora\IslandoraUtils; /** * Checks whether node has fields that qualify it as an "Islandora" node. * * @Condition( * id = "node_is_islandora_object", - * label = @Translation("Node is an Islandora object"), + * label = @Translation("Node is an Islandora node"), * context_definitions = { * "node" = @ContextDefinition("entity:node", required = TRUE , label = @Translation("node")) * } @@ -19,6 +20,30 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class NodeIsIslandoraObject extends ConditionPluginBase implements ContainerFactoryPluginInterface { + /** + * Islandora Utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + + /** + * Constructs a Node is Islandora Condition plugin. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\islandora\IslandoraUtils $islandora_utils + * Islandora utilities. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, IslandoraUtils $islandora_utils) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->utils = $islandora_utils; + } + /** * {@inheritdoc} */ @@ -26,7 +51,8 @@ class NodeIsIslandoraObject extends ConditionPluginBase implements ContainerFact return new static( $configuration, $plugin_id, - $plugin_definition + $plugin_definition, + $container->get('islandora.utils') ); } @@ -38,8 +64,8 @@ class NodeIsIslandoraObject extends ConditionPluginBase implements ContainerFact if (!$node) { return FALSE; } - // Islandora objects have these two fields. - if ($node->hasField('field_model') && $node->hasField('field_member_of')) { + // Determine if node is Islandora. + if ($this->utils->isIslandoraType('node', $node->bundle())) { return TRUE; } } @@ -49,10 +75,10 @@ class NodeIsIslandoraObject extends ConditionPluginBase implements ContainerFact */ public function summary() { if (!empty($this->configuration['negate'])) { - return $this->t('The node is not an Islandora object.'); + return $this->t('The node is not an Islandora node.'); } else { - return $this->t('The node is an Islandora object.'); + return $this->t('The node is an Islandora node.'); } } diff --git a/src/Plugin/Condition/NodeReferencedByNode.php b/src/Plugin/Condition/NodeReferencedByNode.php index a2abac80..d6db01ea 100644 --- a/src/Plugin/Condition/NodeReferencedByNode.php +++ b/src/Plugin/Condition/NodeReferencedByNode.php @@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * @Condition( * id = "node_referenced_by_node", * label = @Translation("Node is referenced by other nodes"), - * context = { + * context_definitions = { * "node" = @ContextDefinition("entity:node", required = TRUE , label = @Translation("node")) * } * ) @@ -128,6 +128,7 @@ class NodeReferencedByNode extends ConditionPluginBase implements ContainerFacto $config = FieldStorageConfig::loadByName('node', $reference_field); if ($config) { $id_count = \Drupal::entityQuery('node') + ->accessCheck(TRUE) ->condition($reference_field, $entity->id()) ->count() ->execute(); diff --git a/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php b/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php index 6c6e87da..deb6c769 100644 --- a/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php +++ b/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php @@ -5,6 +5,7 @@ namespace Drupal\islandora\Plugin\Field\FieldFormatter; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Session\AccountInterface; use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatter; use Drupal\islandora\IslandoraUtils; @@ -56,6 +57,8 @@ class IslandoraImageFormatter extends ImageFormatter { * The image style storage. * @param \Drupal\islandora\IslandoraUtils $utils * Islandora utils. + * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator + * The File URL Generator. */ public function __construct( $plugin_id, @@ -67,7 +70,8 @@ class IslandoraImageFormatter extends ImageFormatter { array $third_party_settings, AccountInterface $current_user, EntityStorageInterface $image_style_storage, - IslandoraUtils $utils + IslandoraUtils $utils, + FileUrlGeneratorInterface $file_url_generator ) { parent::__construct( $plugin_id, @@ -78,7 +82,8 @@ class IslandoraImageFormatter extends ImageFormatter { $view_mode, $third_party_settings, $current_user, - $image_style_storage + $image_style_storage, + $file_url_generator ); $this->utils = $utils; } @@ -97,7 +102,8 @@ class IslandoraImageFormatter extends ImageFormatter { $configuration['third_party_settings'], $container->get('current_user'), $container->get('entity_type.manager')->getStorage('image_style'), - $container->get('islandora.utils') + $container->get('islandora.utils'), + $container->get('file_url_generator') ); } diff --git a/src/Plugin/views/field/IntegerWeightSelector.php b/src/Plugin/views/field/IntegerWeightSelector.php index 141d3567..60892c96 100644 --- a/src/Plugin/views/field/IntegerWeightSelector.php +++ b/src/Plugin/views/field/IntegerWeightSelector.php @@ -46,12 +46,13 @@ class IntegerWeightSelector extends FieldPluginBase { $options[$this->getValue($row)] = $this->getValue($row); } - // If we were given some blank values we need to fill + // If we were given some blank values, or less than the + // total_rows for the view, we need to fill // out the option list from 1 through the result count // to make sure we have enough. (Blanks should only appear // at the beginning of the results list.) // Also, blank values will break the selector, remove it. - if (array_key_exists('', $options)) { + if (array_key_exists('', $options) || (count($options) < $this->view->total_rows)) { unset($options['']); for ($i = 1; $i <= $this->view->total_rows; $i++) { $options[$i] = $i; diff --git a/src/Plugin/views/filter/NodeHasMediaUse.php b/src/Plugin/views/filter/NodeHasMediaUse.php new file mode 100644 index 00000000..3c69bddd --- /dev/null +++ b/src/Plugin/views/filter/NodeHasMediaUse.php @@ -0,0 +1,94 @@ + NULL]; + $options['negated'] = ['default' => FALSE]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function validateOptionsForm(&$form, FormStateInterface $form_state) { + $uri = $form_state->getValues()['options']['use_uri']; + $term = \Drupal::service('islandora.utils')->getTermForUri($uri); + if (empty($term)) { + $form_state->setError($form['use_uri'], $this->t('Could not find term with URI: "%uri"', ['%uri' => $uri])); + } + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties(['vid' => 'islandora_media_use']); + $uris = []; + foreach ($terms as $term) { + foreach ($term->get('field_external_uri')->getValue() as $uri) { + $uris[$uri['uri']] = $term->label(); + } + } + + $form['use_uri'] = [ + '#type' => 'select', + '#title' => "Media Use Term", + '#options' => $uris, + '#default_value' => $this->options['use_uri'], + '#required' => TRUE, + ]; + $form['negated'] = [ + '#type' => 'checkbox', + '#title' => 'Negated', + '#description' => $this->t("Return nodes that don't have this use URI"), + '#default_value' => $this->options['negated'], + ]; + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + $operator = ($this->options['negated']) ? "does not have" : "has"; + $term = \Drupal::service('islandora.utils')->getTermForUri($this->options['use_uri']); + $label = (empty($term)) ? 'BROKEN TERM URI' : $term->label(); + return "Node {$operator} a '{$label}' media"; + } + + /** + * {@inheritdoc} + */ + public function query() { + $condition = ($this->options['negated']) ? 'NOT IN' : 'IN'; + $utils = \Drupal::service('islandora.utils'); + $term = $utils->getTermForUri($this->options['use_uri']); + if (empty($term)) { + \Drupal::logger('islandora')->warning('Node Has Media Filter could not find term with URI: "%uri"', ['%uri' => $this->options['use_uri']]); + return; + } + $sub_query = \Drupal::database()->select('media', 'm'); + $sub_query->join('media__field_media_use', 'use', 'm.mid = use.entity_id'); + $sub_query->join('media__field_media_of', 'of', 'm.mid = of.entity_id'); + $sub_query->fields('of', ['field_media_of_target_id']) + ->condition('use.field_media_use_target_id', $term->id()); + $this->query->addWhere(0, 'nid', $sub_query, $condition); + } + +} diff --git a/src/Plugin/views/filter/NodeIsIslandora.php b/src/Plugin/views/filter/NodeIsIslandora.php new file mode 100644 index 00000000..2065c46b --- /dev/null +++ b/src/Plugin/views/filter/NodeIsIslandora.php @@ -0,0 +1,144 @@ +joinHandler = $join_handler; + $this->utils = $islandora_utils; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, $plugin_id, $plugin_definition, + $container->get('plugin.manager.views.join'), + $container->get('islandora.utils'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + return [ + 'negated' => ['default' => FALSE], + ]; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $types = []; + foreach ($this->entityTypeBundleInfo->getBundleInfo('node') as $bundle_id => $bundle) { + if ($this->utils->isIslandoraType('node', $bundle_id)) { + $types[] = "{$bundle['label']} ($bundle_id)"; + } + } + $types_list = implode(', ', $types); + $form['info'] = [ + '#type' => 'item', + '#title' => 'Information', + '#description' => t("Configured Islandora bundles: @types", ['@types' => $types_list]), + ]; + $form['negated'] = [ + '#type' => 'checkbox', + '#title' => 'Negated', + '#description' => $this->t("Return nodes that don't have islandora fields"), + '#default_value' => $this->options['negated'], + ]; + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + $operator = ($this->options['negated']) ? "is not" : "is"; + return "Node {$operator} an islandora node"; + } + + /** + * {@inheritdoc} + */ + public function query() { + $types = []; + foreach (array_keys($this->entityTypeBundleInfo->getBundleInfo('node')) as $bundle_id) { + if ($this->utils->isIslandoraType('node', $bundle_id)) { + $types[] = $bundle_id; + } + } + $condition = ($this->options['negated']) ? 'NOT IN' : 'IN'; + $query_base_table = $this->relationship ?: $this->view->storage->get('base_table'); + + $definition = [ + 'table' => 'node', + 'type' => 'LEFT', + 'field' => 'nid', + 'left_table' => $query_base_table, + 'left_field' => 'nid', + ]; + $join = $this->joinHandler->createInstance('standard', $definition); + $node_table_alias = $this->query->addTable('node', $this->relationship, $join); + $this->query->addWhere($this->options['group'], "$node_table_alias.type", $types, $condition); + } + +} diff --git a/tests/fixtures/config/core.entity_form_display.media.test_media_type.default.yml b/tests/fixtures/config/core.entity_form_display.media.test_media_type.default.yml index 19fe419b..ea8eac00 100644 --- a/tests/fixtures/config/core.entity_form_display.media.test_media_type.default.yml +++ b/tests/fixtures/config/core.entity_form_display.media.test_media_type.default.yml @@ -1,9 +1,11 @@ +uuid: 9151a0fe-7729-4943-b506-dd6f8d12ceac langcode: en status: true dependencies: config: + - field.field.media.test_media_type.field_media_file - field.field.media.test_media_type.field_media_of - - field.field.media.test_media_type.field_tags + - field.field.media.test_media_type.field_media_use - media.type.test_media_type module: - path @@ -34,15 +36,17 @@ content: region: content settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } - field_tags: + field_media_use: type: entity_reference_autocomplete weight: 3 region: content settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } @@ -64,6 +68,7 @@ content: weight: 4 settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' region: content diff --git a/tests/fixtures/config/core.entity_form_display.node.test_type.default.yml b/tests/fixtures/config/core.entity_form_display.node.test_type.default.yml index 2560ec6e..68724265 100644 --- a/tests/fixtures/config/core.entity_form_display.node.test_type.default.yml +++ b/tests/fixtures/config/core.entity_form_display.node.test_type.default.yml @@ -1,12 +1,13 @@ +uuid: 90a6909f-a2aa-44e8-8b61-4cd54ec6974f langcode: en status: true dependencies: config: - field.field.node.test_type.field_member_of + - field.field.node.test_type.field_model - node.type.test_type module: - path - - text id: node.test_type.default targetEntityType: node bundle: test_type @@ -19,14 +20,25 @@ content: settings: { } third_party_settings: { } field_member_of: + type: entity_reference_autocomplete weight: 122 + region: content settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' third_party_settings: { } + field_model: type: entity_reference_autocomplete + weight: 123 region: content + settings: + match_operator: CONTAINS + match_limit: 10 + size: 60 + placeholder: '' + third_party_settings: { } langcode: type: language_select weight: 2 @@ -42,24 +54,24 @@ content: third_party_settings: { } promote: type: boolean_checkbox - settings: - display_label: true weight: 15 region: content + settings: + display_label: true third_party_settings: { } status: type: boolean_checkbox - settings: - display_label: true weight: 120 region: content + settings: + display_label: true third_party_settings: { } sticky: type: boolean_checkbox - settings: - display_label: true weight: 16 region: content + settings: + display_label: true third_party_settings: { } title: type: string_textfield @@ -72,10 +84,11 @@ content: uid: type: entity_reference_autocomplete weight: 5 + region: content settings: match_operator: CONTAINS + match_limit: 10 size: 60 placeholder: '' - region: content third_party_settings: { } hidden: { } diff --git a/tests/fixtures/config/core.entity_form_display.node.test_type.secondary.yml b/tests/fixtures/config/core.entity_form_display.node.test_type.secondary.yml index b1fdb88e..f8f05beb 100644 --- a/tests/fixtures/config/core.entity_form_display.node.test_type.secondary.yml +++ b/tests/fixtures/config/core.entity_form_display.node.test_type.secondary.yml @@ -1,12 +1,12 @@ +uuid: e24c2b3c-60e4-4ff5-99cb-80e5e67e7b04 langcode: en status: true dependencies: config: + - core.entity_form_mode.node.secondary - field.field.node.test_type.field_member_of + - field.field.node.test_type.field_model - node.type.test_type - module: - - path - - text id: node.test_type.secondary targetEntityType: node bundle: test_type @@ -23,6 +23,8 @@ content: hidden: created: true field_media: true + field_member_of: true + field_model: true field_node: true langcode: true path: true diff --git a/tests/fixtures/config/core.entity_form_mode.node.secondary.yml b/tests/fixtures/config/core.entity_form_mode.node.secondary.yml index 07f45bbe..e1fc7634 100644 --- a/tests/fixtures/config/core.entity_form_mode.node.secondary.yml +++ b/tests/fixtures/config/core.entity_form_mode.node.secondary.yml @@ -1,9 +1,10 @@ +uuid: d9f22219-ff4c-48cc-a98a-6ccaad7a880d langcode: en status: true dependencies: module: - node id: node.secondary -label: Secondary +label: Secondary targetEntityType: node -cache: true +cache: true \ No newline at end of file diff --git a/tests/fixtures/config/core.entity_view_display.node.test_type.default.yml b/tests/fixtures/config/core.entity_view_display.node.test_type.default.yml index cf798265..e4414e61 100644 --- a/tests/fixtures/config/core.entity_view_display.node.test_type.default.yml +++ b/tests/fixtures/config/core.entity_view_display.node.test_type.default.yml @@ -1,11 +1,12 @@ +uuid: 36f4aecf-0e14-4281-a213-ca7d129da52a langcode: en status: true dependencies: config: - field.field.node.test_type.field_member_of + - field.field.node.test_type.field_model - node.type.test_type module: - - text - user id: node.test_type.default targetEntityType: node @@ -13,14 +14,24 @@ bundle: test_type mode: default content: field_member_of: - weight: 102 + type: entity_reference_label label: above settings: link: true third_party_settings: { } + weight: 102 + region: content + field_model: type: entity_reference_label + label: above + settings: + link: true + third_party_settings: { } + weight: 103 region: content links: + settings: { } + third_party_settings: { } weight: 100 region: content hidden: diff --git a/tests/fixtures/config/core.entity_view_display.node.test_type.teaser.yml b/tests/fixtures/config/core.entity_view_display.node.test_type.teaser.yml index d67060f7..f7295428 100644 --- a/tests/fixtures/config/core.entity_view_display.node.test_type.teaser.yml +++ b/tests/fixtures/config/core.entity_view_display.node.test_type.teaser.yml @@ -1,19 +1,25 @@ -uuid: 0308339a-a9e5-4a04-8ce2-9f62ed504e34 +uuid: b337f462-8e64-4853-be65-9e03b94515bf langcode: en status: true dependencies: config: - core.entity_view_mode.node.teaser + - field.field.node.test_type.field_member_of + - field.field.node.test_type.field_model - node.type.test_type module: - - text - user id: node.test_type.teaser targetEntityType: node bundle: test_type mode: teaser content: + links: + settings: { } + third_party_settings: { } + weight: 100 + region: content hidden: - body: true - links: true - langcode: true + field_member_of: true + field_model: true + langcode: true \ No newline at end of file diff --git a/tests/fixtures/config/rest.resource.entity.file.yml b/tests/fixtures/config/rest.resource.entity.file.yml index 6a136c3c..dbd6bb62 100644 --- a/tests/fixtures/config/rest.resource.entity.file.yml +++ b/tests/fixtures/config/rest.resource.entity.file.yml @@ -1,3 +1,4 @@ +uuid: 11c4e25e-6b06-4270-b934-243e4f4aade1 langcode: en status: true dependencies: @@ -26,3 +27,4 @@ configuration: supported_auth: - basic_auth - jwt_auth + - cookie diff --git a/tests/fixtures/config/rest.resource.entity.media.yml b/tests/fixtures/config/rest.resource.entity.media.yml index 3ed0286e..cd89243d 100644 --- a/tests/fixtures/config/rest.resource.entity.media.yml +++ b/tests/fixtures/config/rest.resource.entity.media.yml @@ -1,3 +1,4 @@ +uuid: 9a5633b1-6a1a-40b2-8482-c24cf44122ff langcode: en status: true dependencies: @@ -5,7 +6,7 @@ dependencies: - basic_auth - jsonld - jwt - - media_entity + - media - serialization - user id: entity.media diff --git a/tests/fixtures/config/rest.resource.entity.node.yml b/tests/fixtures/config/rest.resource.entity.node.yml index e7d4c7cc..a3e253e2 100644 --- a/tests/fixtures/config/rest.resource.entity.node.yml +++ b/tests/fixtures/config/rest.resource.entity.node.yml @@ -1,3 +1,4 @@ +uuid: 08a90469-0355-4b41-a4d6-cb6b53072b8c langcode: en status: true dependencies: diff --git a/tests/fixtures/config/rest.resource.entity.taxonomy_term.yml b/tests/fixtures/config/rest.resource.entity.taxonomy_term.yml index 25b6fbb2..16d96c3e 100644 --- a/tests/fixtures/config/rest.resource.entity.taxonomy_term.yml +++ b/tests/fixtures/config/rest.resource.entity.taxonomy_term.yml @@ -1,3 +1,4 @@ +uuid: 7534e393-12a7-498c-a4a3-a7bbe4ff9a5d langcode: en status: true dependencies: diff --git a/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml b/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml index 7e298142..77b12ec3 100644 --- a/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml +++ b/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml @@ -2,7 +2,7 @@ name: 'Integer weight test views' type: module description: 'Provides default views for integer weight views tests.' package: Testing -core_version_requirement: ^8 || ^9 +core_version_requirement: ^8 || ^9 || ^10 dependencies: - drupal:node - drupal:views diff --git a/tests/src/Functional/AddChildTest.php b/tests/src/Functional/AddChildTest.php index 9fc2f9e2..ed5282c7 100644 --- a/tests/src/Functional/AddChildTest.php +++ b/tests/src/Functional/AddChildTest.php @@ -9,13 +9,19 @@ namespace Drupal\Tests\islandora\Functional; */ class AddChildTest extends IslandoraFunctionalTestBase { + /** + * The taxonomy term representing "Collection" items. + * + * @var \Drupal\taxonomy\TermInterface + */ + protected $collectionTerm; + /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); - $this->parent = $this->collectionTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ 'name' => 'Collection', 'vid' => $this->testVocabulary->id(), diff --git a/tests/src/Functional/AddMediaToNodeTest.php b/tests/src/Functional/AddMediaToNodeTest.php index 32909775..4b0b62c5 100644 --- a/tests/src/Functional/AddMediaToNodeTest.php +++ b/tests/src/Functional/AddMediaToNodeTest.php @@ -31,7 +31,7 @@ class AddMediaToNodeTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->node = $this->container->get('entity_type.manager')->getStorage('node')->create([ diff --git a/tests/src/Functional/ContentEntityTypeTest.php b/tests/src/Functional/ContentEntityTypeTest.php index 362ff7fb..5ed22948 100644 --- a/tests/src/Functional/ContentEntityTypeTest.php +++ b/tests/src/Functional/ContentEntityTypeTest.php @@ -52,7 +52,8 @@ class ContentEntityTypeTest extends IslandoraFunctionalTestBase { 'name[0][value]' => 'Test Media', 'files[field_media_file_0]' => __DIR__ . '/../../fixtures/test_file.txt', ]; - $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, $this->t('Save')); + $this->drupalGet('media/add/' . $this->testMediaType->id()); + $this->submitForm($values, $this->t('Save')); $this->assertSession()->pageTextNotContains("Hello World!"); } diff --git a/tests/src/Functional/DeleteMediaTest.php b/tests/src/Functional/DeleteMediaTest.php index f112c700..c52eca31 100644 --- a/tests/src/Functional/DeleteMediaTest.php +++ b/tests/src/Functional/DeleteMediaTest.php @@ -16,7 +16,7 @@ class DeleteMediaTest extends IslandoraFunctionalTestBase { * * @var array */ - public static $modules = [ + protected static $modules = [ 'media_test_views', 'context_ui', 'field_ui', @@ -47,11 +47,18 @@ class DeleteMediaTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); + if (version_compare(\Drupal::VERSION, '10.1', '>=')) { + $permissions = ['create media', 'delete any media', 'delete any file']; + } + else { + $permissions = ['create media', 'delete any media']; + } + // Create a test user. - $this->account = $this->createUser(['create media', 'delete any media']); + $this->account = $this->createUser($permissions); list($this->file, $this->media) = $this->makeMediaAndFile($this->account); } diff --git a/tests/src/Functional/DeleteNodeWithMediaAndFile.php b/tests/src/Functional/DeleteNodeWithMediaAndFile.php new file mode 100644 index 00000000..5ee19b7c --- /dev/null +++ b/tests/src/Functional/DeleteNodeWithMediaAndFile.php @@ -0,0 +1,104 @@ +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", + ]); + $file->setPermanent(); + $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'); + + } + +} diff --git a/tests/src/Functional/DerivativeReactionTest.php b/tests/src/Functional/DerivativeReactionTest.php index e1b1c827..00e0e5ae 100644 --- a/tests/src/Functional/DerivativeReactionTest.php +++ b/tests/src/Functional/DerivativeReactionTest.php @@ -19,7 +19,7 @@ class DerivativeReactionTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->node = $this->container->get('entity_type.manager')->getStorage('node')->create([ @@ -52,7 +52,8 @@ class DerivativeReactionTest extends IslandoraFunctionalTestBase { 'files[field_media_file_0]' => __DIR__ . '/../../fixtures/test_file.txt', 'field_media_of[0][target_id]' => 'Test Node', ]; - $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, $this->t('Save')); + $this->drupalGet('media/add/' . $this->testMediaType->id()); + $this->submitForm($values, $this->t('Save')); // field_media_of is set and there's a file, so derivatives should fire. $this->assertSession()->pageTextContains("Hello World!"); diff --git a/tests/src/Functional/GenerateDerivativeTestBase.php b/tests/src/Functional/GenerateDerivativeTestBase.php index 0f67d591..c5ec9701 100644 --- a/tests/src/Functional/GenerateDerivativeTestBase.php +++ b/tests/src/Functional/GenerateDerivativeTestBase.php @@ -29,7 +29,7 @@ abstract class GenerateDerivativeTestBase extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->createUserAndLogin(); diff --git a/tests/src/Functional/IndexingTest.php b/tests/src/Functional/IndexingTest.php index e995329d..ff215281 100644 --- a/tests/src/Functional/IndexingTest.php +++ b/tests/src/Functional/IndexingTest.php @@ -12,7 +12,7 @@ class IndexingTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Create an action that dsm's "Goodbye, Cruel World!". @@ -63,9 +63,10 @@ class IndexingTest extends IslandoraFunctionalTestBase { // Add the Goodbye World reaction. $this->addPresetReaction('test', 'delete', 'goodbye_world'); + $this->drupalGet("$url/delete"); // Delete the node. - $this->drupalPostForm("$url/delete", [], $this->t('Delete')); + $this->submitForm([], $this->t('Delete')); $this->assertSession()->statusCodeEquals(200); // Confirm Goodbye, Cruel World! is printed to the screen. diff --git a/tests/src/Functional/IslandoraFunctionalTestBase.php b/tests/src/Functional/IslandoraFunctionalTestBase.php index c154c5c2..016788d0 100644 --- a/tests/src/Functional/IslandoraFunctionalTestBase.php +++ b/tests/src/Functional/IslandoraFunctionalTestBase.php @@ -88,7 +88,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Delete the node rest config that's bootstrapped with Drupal. @@ -278,11 +278,13 @@ EOD; * Creates a test context. */ protected function createContext($label, $name) { - $this->drupalPostForm('admin/structure/context/add', [ + $this->drupalGet('admin/structure/context/add'); + $values = [ 'label' => $label, 'name' => $name, - ], - $this->t('Save')); + ]; + $this->submitForm($values, 'Save'); + $this->assertSession()->statusCodeEquals(200); } @@ -312,7 +314,8 @@ EOD; * Create a new node by posting its add form. */ protected function postNodeAddForm($bundle_id, $values, $button_text) { - $this->drupalPostForm("node/add/$bundle_id", $values, $this->t('@text', ['@text' => $button_text])); + $this->drupalGet("node/add/$bundle_id"); + $this->submitForm($values, $this->t('@text', ['@text' => $button_text])); $this->assertSession()->statusCodeEquals(200); } @@ -320,7 +323,8 @@ EOD; * Create a new node by posting its add form. */ protected function postTermAddForm($taxomony_id, $values, $button_text) { - $this->drupalPostForm("admin/structure/taxonomy/manage/$taxomony_id/add", $values, $this->t('@text', ['@text' => $button_text])); + $this->drupalGet("admin/structure/taxonomy/manage/$taxomony_id/add"); + $this->submitForm($values, $this->t('@text', ['@text' => $button_text])); $this->assertSession()->statusCodeEquals(200); } @@ -328,7 +332,8 @@ EOD; * Edits a node by posting its edit form. */ protected function postEntityEditForm($entity_url, $values, $button_text) { - $this->drupalPostForm("$entity_url/edit", $values, $this->t('@text', ['@text' => $button_text])); + $this->drupalGet("$entity_url/edit"); + $this->submitForm($values, $this->t('@text', ['@text' => $button_text])); $this->assertSession()->statusCodeEquals(200); } @@ -433,8 +438,8 @@ EOD; 'uri' => "public://test_file.txt", 'filename' => "test_file.txt", 'filemime' => "text/plain", - 'status' => FILE_STATUS_PERMANENT, ]); + $file->setPermanent(); $file->save(); // Get the source field for the media. diff --git a/tests/src/Functional/IslandoraImageFormatterTest.php b/tests/src/Functional/IslandoraImageFormatterTest.php index 2793cf49..1b40f7a8 100644 --- a/tests/src/Functional/IslandoraImageFormatterTest.php +++ b/tests/src/Functional/IslandoraImageFormatterTest.php @@ -27,13 +27,19 @@ class IslandoraImageFormatterTest extends IslandoraFunctionalTestBase { $testImageMediaType = $this->createMediaType('image', ['id' => 'test_image_media_type']); $testImageMediaType->save(); $this->createEntityReferenceField('media', $testImageMediaType->id(), 'field_media_of', 'Media Of', 'node', 'default', [], 2); - // Set the display mode to use the islandora_image formatter. // Also, only show the image on display to remove clutter. $display_options = [ 'type' => 'islandora_image', - 'settings' => ['image_style' => NULL, 'image_link' => 'content'], + 'settings' => [ + 'image_style' => '', + 'image_link' => 'content', + 'image_loading' => [ + 'attribute' => 'eager', + ], + ], ]; + $display = $this->container->get('entity_display.repository')->getViewDisplay('media', $testImageMediaType->id(), 'default'); $display->setComponent('field_media_image', $display_options) ->removeComponent('created') @@ -47,15 +53,14 @@ class IslandoraImageFormatterTest extends IslandoraFunctionalTestBase { 'title' => 'Test Node', ]); $node->save(); - // Make a 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->setPermanent(); $file->save(); // Make the media, and associate it with the image and node. @@ -87,7 +92,7 @@ class IslandoraImageFormatterTest extends IslandoraFunctionalTestBase { ':title' => 'Some Title', ] ); - $this->assertEqual(count($elements), 1, 'Image linked to content formatter displaying points to Node and not Media.'); + $this->assertEquals(count($elements), 1, 'Image linked to content formatter displaying points to Node and not Media.'); } } diff --git a/tests/src/Functional/IslandoraSettingsFormTest.php b/tests/src/Functional/IslandoraSettingsFormTest.php index 92cfc6a2..80a327af 100644 --- a/tests/src/Functional/IslandoraSettingsFormTest.php +++ b/tests/src/Functional/IslandoraSettingsFormTest.php @@ -14,7 +14,7 @@ class IslandoraSettingsFormTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Create a test user. @@ -36,20 +36,25 @@ class IslandoraSettingsFormTest extends IslandoraFunctionalTestBase { $this->assertSession()->statusCodeEquals(200); $this->assertSession()->pageTextContains("JWT Expiry"); $this->assertSession()->fieldValueEquals('edit-jwt-expiry', '+2 hour'); + $this->drupalGet('/admin/config/islandora/core'); // Blank is not allowed. - $this->drupalPostForm('/admin/config/islandora/core', ['edit-jwt-expiry' => ""], $this->t('Save configuration')); + $this->submitForm(['edit-jwt-expiry' => ""], $this->t('Save configuration')); $this->assertSession()->pageTextContainsOnce('"" is not a valid time or interval expression.'); + $this->drupalGet('/admin/config/islandora/core'); // Negative is not allowed. - $this->drupalPostForm('/admin/config/islandora/core', ['edit-jwt-expiry' => "-2 hours"], $this->t('Save configuration')); + $this->submitForm(['edit-jwt-expiry' => "-2 hours"], $this->t('Save configuration')); $this->assertSession()->pageTextContainsOnce('Time or interval expression cannot be negative'); + $this->drupalGet('/admin/config/islandora/core'); // Must include an integer value. - $this->drupalPostForm('/admin/config/islandora/core', ['edit-jwt-expiry' => "last hour"], $this->t('Save configuration')); + $this->submitForm(['edit-jwt-expiry' => "last hour"], $this->t('Save configuration')); $this->assertSession()->pageTextContainsOnce('No numeric interval specified, for example "1 day"'); + $this->drupalGet('/admin/config/islandora/core'); // Must have an accepted interval. - $this->drupalPostForm('/admin/config/islandora/core', ['edit-jwt-expiry' => "1 fortnight"], $this->t('Save configuration')); + $this->submitForm(['edit-jwt-expiry' => "1 fortnight"], $this->t('Save configuration')); $this->assertSession()->pageTextContainsOnce('No time interval found, please include one of'); + $this->drupalGet('/admin/config/islandora/core'); // Test a valid setting. - $this->drupalPostForm('/admin/config/islandora/core', ['edit-jwt-expiry' => "2 weeks"], $this->t('Save configuration')); + $this->submitForm(['edit-jwt-expiry' => "2 weeks"], $this->t('Save configuration')); $this->assertSession()->pageTextContainsOnce('The configuration options have been saved.'); } diff --git a/tests/src/Functional/JsonldSelfReferenceReactionTest.php b/tests/src/Functional/JsonldSelfReferenceReactionTest.php index f3c88271..1b4e24ec 100644 --- a/tests/src/Functional/JsonldSelfReferenceReactionTest.php +++ b/tests/src/Functional/JsonldSelfReferenceReactionTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\islandora\Functional; +use function GuzzleHttp\json_decode; + /** * Class MappingUriPredicateReactionTest. * @@ -10,10 +12,17 @@ namespace Drupal\Tests\islandora\Functional; */ class JsonldSelfReferenceReactionTest extends IslandoraFunctionalTestBase { + /** + * An RDF Mapping object. + * + * @var \Drupal\rdf\Entity\RdfMapping + */ + protected $rdfMapping; + /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $types = ['schema:Thing']; @@ -61,7 +70,7 @@ class JsonldSelfReferenceReactionTest extends IslandoraFunctionalTestBase { $contents = $this->drupalGet($url . '?_format=jsonld'); $this->assertSession()->statusCodeEquals(200); - $json = \GuzzleHttp\json_decode($contents, TRUE); + $json = json_decode($contents, TRUE); $this->assertArrayHasKey('http://purl.org/dc/terms/title', $json['@graph'][0], 'Missing dcterms:title key'); $this->assertEquals( @@ -103,7 +112,7 @@ class JsonldSelfReferenceReactionTest extends IslandoraFunctionalTestBase { drupal_flush_all_caches(); $new_contents = $this->drupalGet($url . '?_format=jsonld'); - $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $json = json_decode($new_contents, TRUE); $this->assertEquals( 'Test Node', $json['@graph'][0]['http://purl.org/dc/terms/title'][0]['@value'], @@ -123,7 +132,7 @@ class JsonldSelfReferenceReactionTest extends IslandoraFunctionalTestBase { $this->assertSession() ->pageTextContains("The context $context_name has been saved"); $new_contents = $this->drupalGet($url . '?_format=jsonld'); - $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $json = json_decode($new_contents, TRUE); $this->assertEquals( 'Test Node', $json['@graph'][0]['http://purl.org/dc/terms/title'][0]['@value'], @@ -161,7 +170,7 @@ class JsonldSelfReferenceReactionTest extends IslandoraFunctionalTestBase { $contents = $this->drupalGet($media_url . '?_format=jsonld'); $this->assertSession()->statusCodeEquals(200); - $json = \GuzzleHttp\json_decode($contents, TRUE); + $json = json_decode($contents, TRUE); $this->assertEquals( "$media_url?_format=jsonld", $json['@graph'][0]['@id'], @@ -186,7 +195,7 @@ class JsonldSelfReferenceReactionTest extends IslandoraFunctionalTestBase { drupal_flush_all_caches(); $new_contents = $this->drupalGet($media_url . '?_format=jsonld'); - $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $json = json_decode($new_contents, TRUE); $this->assertEquals( "$media_url?_format=jsonld", $json['@graph'][0]['http://www.iana.org/assignments/relation/describedby'][0]['@id'], diff --git a/tests/src/Functional/JsonldTypeAlterReactionTest.php b/tests/src/Functional/JsonldTypeAlterReactionTest.php index e5d21abc..75ae41dd 100644 --- a/tests/src/Functional/JsonldTypeAlterReactionTest.php +++ b/tests/src/Functional/JsonldTypeAlterReactionTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\islandora\Functional; +use function GuzzleHttp\json_decode; + /** * Tests Jsonld Alter Reaction. * @@ -20,17 +22,45 @@ class JsonldTypeAlterReactionTest extends JsonldSelfReferenceReactionTest { 'administer node fields', ]); $this->drupalLogin($account); + $this->drupalGet('admin/structure/types/manage/test_type/fields/add-field'); // Add the typed predicate we will select in the reaction config. // Taken from FieldUiTestTrait->fieldUIAddNewField. - $this->drupalPostForm('admin/structure/types/manage/test_type/fields/add-field', [ - 'new_storage_type' => 'string', - 'label' => 'Typed Predicate', - 'field_name' => 'type_predicate', - ], $this->t('Save and continue')); - $this->drupalPostForm(NULL, [], $this->t('Save field settings')); - $this->drupalPostForm(NULL, [], $this->t('Save settings')); - $this->assertRaw('field_type_predicate', 'Redirected to "Manage fields" page.'); + if (version_compare(\Drupal::VERSION, '10.2.x-dev', 'lt')) { + $this->submitForm([ + 'new_storage_type' => 'string', + 'label' => 'Typed Predicate', + 'field_name' => 'type_predicate', + ], 'Save and continue'); + $this->submitForm([], $this->t('Save field settings')); + } + else { + $this->getSession()->getPage()->selectFieldOption('new_storage_type', 'plain_text'); + // First need to submit the form with the elements displayed + // on initial page load. The form is using AJAX to send a second element + // after we selected the radio button above + // we can instead get the second element by submitting the form + // and having it throw an error since the required field is missing. + // @todo refactor this as a functional javascript test. + $this->submitForm([ + 'new_storage_type' => 'plain_text', + 'label' => 'Typed Predicate', + 'field_name' => 'type_predicate', + ], $this->t('Continue')); + + // Now we can proceed, selecting the plain text (i.e. string) + // for the second element now that the element is displayed after + // the initial form submission. + $this->getSession()->getPage()->selectFieldOption('group_field_options_wrapper', 'string'); + $this->submitForm([ + 'new_storage_type' => 'plain_text', + 'label' => 'Typed Predicate', + 'field_name' => 'type_predicate', + 'group_field_options_wrapper' => 'string', + ], $this->t('Continue')); + } + $this->submitForm([], $this->t('Save settings')); + $this->assertSession()->responseContains('field_type_predicate'); // Add the test node. $this->postNodeAddForm('test_type', [ @@ -46,7 +76,7 @@ class JsonldTypeAlterReactionTest extends JsonldSelfReferenceReactionTest { $contents = $this->drupalGet($url . '?_format=jsonld'); $this->assertSession()->statusCodeEquals(200); - $json = \GuzzleHttp\json_decode($contents, TRUE); + $json = json_decode($contents, TRUE); $this->assertArrayHasKey('@type', $json['@graph'][0], 'Missing @type'); $this->assertEquals( @@ -81,7 +111,7 @@ class JsonldTypeAlterReactionTest extends JsonldSelfReferenceReactionTest { // Check for the new @type from the field_type_predicate value. $new_contents = $this->drupalGet($url . '?_format=jsonld'); - $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $json = json_decode($new_contents, TRUE); $this->assertTrue( in_array('http://schema.org/Organization', $json['@graph'][0]['@type']), 'Missing altered @type value of http://schema.org/Organization' diff --git a/tests/src/Functional/LinkHeaderTest.php b/tests/src/Functional/LinkHeaderTest.php index 7cb741d5..98b36c68 100644 --- a/tests/src/Functional/LinkHeaderTest.php +++ b/tests/src/Functional/LinkHeaderTest.php @@ -42,7 +42,7 @@ class LinkHeaderTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $account = $this->createUserAndLogin(); diff --git a/tests/src/Functional/MediaSourceUpdateTest.php b/tests/src/Functional/MediaSourceUpdateTest.php index fdea6aef..3938e9b4 100644 --- a/tests/src/Functional/MediaSourceUpdateTest.php +++ b/tests/src/Functional/MediaSourceUpdateTest.php @@ -35,7 +35,7 @@ class MediaSourceUpdateTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Make a user with appropriate permissions. @@ -52,8 +52,8 @@ class MediaSourceUpdateTest extends IslandoraFunctionalTestBase { 'uri' => "public://test_file.txt", 'filename' => "test_file.txt", 'filemime' => "text/plain", - 'status' => FILE_STATUS_PERMANENT, ]); + $this->file->setPermanent(); $this->file->save(); // Get the source field for the media. diff --git a/tests/src/Functional/NodeHasTermTest.php b/tests/src/Functional/NodeHasTermTest.php index eff5b5c3..2b4ee16f 100644 --- a/tests/src/Functional/NodeHasTermTest.php +++ b/tests/src/Functional/NodeHasTermTest.php @@ -13,7 +13,7 @@ class NodeHasTermTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); diff --git a/tests/src/Functional/ViewModeAlterReactionTest.php b/tests/src/Functional/ViewModeAlterReactionTest.php index 72cdfe44..19660bda 100644 --- a/tests/src/Functional/ViewModeAlterReactionTest.php +++ b/tests/src/Functional/ViewModeAlterReactionTest.php @@ -26,7 +26,7 @@ class ViewModeAlterReactionTest extends IslandoraFunctionalTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Node to be referenced via member of. diff --git a/tests/src/FunctionalJavascript/IntegerWeightTest.php b/tests/src/FunctionalJavascript/IntegerWeightTest.php index ba289aa4..2572c191 100644 --- a/tests/src/FunctionalJavascript/IntegerWeightTest.php +++ b/tests/src/FunctionalJavascript/IntegerWeightTest.php @@ -80,7 +80,7 @@ class IntegerWeightTest extends WebDriverTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->adminUser = $this->drupalCreateUser( diff --git a/tests/src/Kernel/EventGeneratorTest.php b/tests/src/Kernel/EventGeneratorTest.php index c423cda3..34a5ae92 100644 --- a/tests/src/Kernel/EventGeneratorTest.php +++ b/tests/src/Kernel/EventGeneratorTest.php @@ -41,7 +41,7 @@ class EventGeneratorTest extends IslandoraKernelTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Create a test user. @@ -49,7 +49,7 @@ class EventGeneratorTest extends IslandoraKernelTestBase { $test_type = NodeType::create([ 'type' => 'test_type', - 'label' => 'Test Type', + 'name' => 'Test Type', ]); $test_type->save(); @@ -116,7 +116,6 @@ class EventGeneratorTest extends IslandoraKernelTestBase { ['event' => 'delete', 'queue' => 'islandora-indexing-fcrepo-delete'] ); $msg = json_decode($json, TRUE); - $msg = json_decode($json, TRUE); $this->assertBasicStructure($msg); $this->assertTrue($msg["type"] == "Delete", "Event must be of type 'Delete'."); diff --git a/tests/src/Kernel/FedoraAdapterTest.php b/tests/src/Kernel/FedoraAdapterTest.php index 10c5beb1..6d8fee28 100644 --- a/tests/src/Kernel/FedoraAdapterTest.php +++ b/tests/src/Kernel/FedoraAdapterTest.php @@ -2,12 +2,15 @@ namespace Drupal\Tests\islandora\Kernel; +use Prophecy\PhpUnit\ProphecyTrait; +use GuzzleHttp\Psr7\Utils; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\islandora\Flysystem\Adapter\FedoraAdapter; use GuzzleHttp\Psr7\Response; use Islandora\Chullo\IFedoraApi; use League\Flysystem\Config; use Prophecy\Argument; -use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; +use Symfony\Component\Mime\MimeTypeGuesserInterface; /** * Tests the Fedora adapter for Flysystem. @@ -17,6 +20,31 @@ use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; */ class FedoraAdapterTest extends IslandoraKernelTestBase { + use ProphecyTrait; + /** + * A mimetype guesser prophecy. + * + * @var \Prophecy\Prophecy\ObjectProphecy + */ + private $mimeGuesser; + + /** + * A logger prophecy. + * + * @var \Prophecy\Prophecy\ObjectProphecy + */ + private $logger; + + /** + * {@inheritdoc} + */ + public function setUp(): void { + parent::setUp(); + $this->mimeGuesser = $this->prophesize(MimeTypeGuesserInterface::class) + ->reveal(); + $this->logger = $this->prophesize(LoggerChannelInterface::class)->reveal(); + } + /** * Shared functionality for an adapter. */ @@ -32,13 +60,8 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { ]); $prophecy->getHeader('Content-Type')->willReturn(['text/plain']); $prophecy->getHeader('Content-Length')->willReturn([strlen("DERP")]); - // phpcs:disable - if (class_exists(\GuzzleHttp\Psr7\Utils::class)) { - $prophecy->getBody()->willReturn(\GuzzleHttp\Psr7\Utils::streamFor("DERP")); - } else { - $prophecy->getBody()->willReturn(\GuzzleHttp\Psr7\stream_for("DERP")); - } - // phpcs:enable + $prophecy->getBody()->willReturn(Utils::streamFor("DERP")); + return $prophecy; } @@ -55,10 +78,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $prophecy->getResource('', ['Connection' => 'close'])->willReturn($response); $api = $prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -73,10 +93,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $prophecy->getResource('', ['Connection' => 'close'])->willReturn($response); $api = $prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -98,10 +115,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $prophecy->getResourceHeaders('', ['Connection' => 'close'])->willReturn($response); $api = $prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -126,10 +140,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -149,10 +160,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -180,10 +188,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -199,10 +204,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $fedora_prophecy->getResourceHeaders('', ['Connection' => 'close'])->willReturn($prophecy->reveal()); $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -218,10 +220,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -236,7 +235,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $head_prophecy = $this->prophesize(Response::class); $head_prophecy->getStatusCode()->willReturn(410); $head_prophecy->getHeader('Link') - ->willReturn('; rel="hasTombstone"'); + ->willReturn(['; rel="hasTombstone"']); $tombstone_prophecy = $this->prophesize(Response::class); $tombstone_prophecy->getStatusCode()->willReturn(204); @@ -249,10 +248,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -267,7 +263,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $head_prophecy = $this->prophesize(Response::class); $head_prophecy->getStatusCode()->willReturn(410); $head_prophecy->getHeader('Link') - ->willReturn('; rel="hasTombstone"'); + ->willReturn(['; rel="hasTombstone"']); $tombstone_prophecy = $this->prophesize(Response::class); $tombstone_prophecy->getStatusCode()->willReturn(500); @@ -280,10 +276,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - return new FedoraAdapter($api, $mime_guesser); + return new FedoraAdapter($api, $this->mimeGuesser, $this->logger); } /** @@ -361,7 +354,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $metadata = $adapter->read(''); $this->assertFileMetadata($metadata); - $this->assertTrue($metadata['contents'] == "DERP", "Expecting 'content' of 'DERP', received '${metadata['contents']}'"); + $this->assertTrue($metadata['contents'] == "DERP", "Expecting 'content' of 'DERP', received '{$metadata['contents']}'"); } /** @@ -644,10 +637,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - $adapter = new FedoraAdapter($api, $mime_guesser); + $adapter = new FedoraAdapter($api, $this->mimeGuesser, $this->logger); $this->assertTrue($adapter->rename('', '') == TRUE, "rename() must return TRUE on success"); } @@ -664,10 +654,7 @@ class FedoraAdapterTest extends IslandoraKernelTestBase { $api = $fedora_prophecy->reveal(); - $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class) - ->reveal(); - - $adapter = new FedoraAdapter($api, $mime_guesser); + $adapter = new FedoraAdapter($api, $this->mimeGuesser, $this->logger); $this->assertTrue($adapter->createDir('', $this->prophesize(Config::class) ->reveal()) == FALSE, "createDir() must return FALSE on fail"); diff --git a/tests/src/Kernel/FedoraPluginTest.php b/tests/src/Kernel/FedoraPluginTest.php index 67491507..92a8137c 100644 --- a/tests/src/Kernel/FedoraPluginTest.php +++ b/tests/src/Kernel/FedoraPluginTest.php @@ -2,11 +2,13 @@ namespace Drupal\Tests\islandora\Kernel; +use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\islandora\Flysystem\Fedora; -use League\Flysystem\AdapterInterface; use Islandora\Chullo\IFedoraApi; +use League\Flysystem\AdapterInterface; +use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\ResponseInterface; -use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; +use Symfony\Component\Mime\MimeTypeGuesserInterface; /** * Tests the Fedora plugin for Flysystem. @@ -16,6 +18,8 @@ use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; */ class FedoraPluginTest extends IslandoraKernelTestBase { + use ProphecyTrait; + /** * Mocks up a plugin. */ @@ -32,8 +36,9 @@ class FedoraPluginTest extends IslandoraKernelTestBase { $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class)->reveal(); $language_manager = $this->container->get('language_manager'); + $logger = $this->prophesize(LoggerChannelInterface::class)->reveal(); - return new Fedora($api, $mime_guesser, $language_manager); + return new Fedora($api, $mime_guesser, $language_manager, $logger); } /** diff --git a/tests/src/Kernel/IslandoraKernelTestBase.php b/tests/src/Kernel/IslandoraKernelTestBase.php index 5a95cb68..1c98db3e 100644 --- a/tests/src/Kernel/IslandoraKernelTestBase.php +++ b/tests/src/Kernel/IslandoraKernelTestBase.php @@ -12,7 +12,7 @@ abstract class IslandoraKernelTestBase extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = [ + protected static $modules = [ 'system', 'user', 'field', @@ -43,7 +43,7 @@ abstract class IslandoraKernelTestBase extends KernelTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); // Bootstrap minimal Drupal environment to run the tests. diff --git a/tests/src/Kernel/JwtEventSubscriberTest.php b/tests/src/Kernel/JwtEventSubscriberTest.php index f97eab9f..9493ab78 100644 --- a/tests/src/Kernel/JwtEventSubscriberTest.php +++ b/tests/src/Kernel/JwtEventSubscriberTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\islandora\Kernel; +use Prophecy\PhpUnit\ProphecyTrait; use Drupal\jwt\Authentication\Event\JwtAuthGenerateEvent; use Drupal\jwt\Authentication\Event\JwtAuthValidEvent; use Drupal\jwt\Authentication\Event\JwtAuthValidateEvent; @@ -19,6 +20,7 @@ use Drupal\islandora\EventSubscriber\JwtEventSubscriber; */ class JwtEventSubscriberTest extends IslandoraKernelTestBase { + use ProphecyTrait; use UserCreationTrait; /** @@ -31,7 +33,7 @@ class JwtEventSubscriberTest extends IslandoraKernelTestBase { /** * {@inheritdoc} */ - public function setUp() { + public function setUp(): void { parent::setUp(); $this->user = $this->createUser();