Browse Source

Merge branch '2.x' of https://github.com/Islandora/islandora into 2.x

pull/999/head
ajstanley 10 months ago
parent
commit
c149781da0
  1. 2
      .github/PULL_REQUEST_TEMPLATE.md
  2. 23
      .github/workflows/build-2.x.yml
  3. 26
      .github/workflows/gitlab-mirror.yml
  4. 35
      README.md
  5. 37
      composer.json
  6. 2
      config/install/islandora.settings.yml
  7. 91
      config/schema/islandora.schema.yml
  8. 3
      css/islandora.css
  9. 43
      islandora.info.yml
  10. 52
      islandora.install
  11. 5
      islandora.libraries.yml
  12. 241
      islandora.module
  13. 16
      islandora.post_update.php
  14. 15
      islandora.routing.yml
  15. 21
      islandora.services.yml
  16. 35
      islandora.tokens.inc
  17. 51
      islandora.views.inc
  18. 4
      modules/islandora_advanced_search/islandora_advanced_search.info.yml
  19. 102
      modules/islandora_advanced_search/islandora_advanced_search.module
  20. 147
      modules/islandora_advanced_search/js/facets/facets-views-ajax.js
  21. 70
      modules/islandora_advanced_search/js/facets/soft-limit.js
  22. 2
      modules/islandora_advanced_search/src/Form/AdvancedSearchForm.php
  23. 2
      modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlock.php
  24. 2
      modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlock.php
  25. 3
      modules/islandora_audio/config/schema/islandora_audio.schema.yml
  26. 3
      modules/islandora_audio/islandora_audio.info.yml
  27. 5
      modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php
  28. 9
      modules/islandora_breadcrumbs/config/install/islandora_breadcrumbs.breadcrumbs.yml
  29. 9
      modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml
  30. 5
      modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml
  31. 18
      modules/islandora_breadcrumbs/islandora_breadcrumbs.install
  32. 5
      modules/islandora_breadcrumbs/islandora_breadcrumbs.links.menu.yml
  33. 7
      modules/islandora_breadcrumbs/islandora_breadcrumbs.routing.yml
  34. 2
      modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml
  35. 132
      modules/islandora_breadcrumbs/src/Form/IslandoraBreadcrumbsSettingsForm.php
  36. 69
      modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php
  37. 4
      modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php
  38. 4
      modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml
  39. 23
      modules/islandora_core_feature/config/install/filehash.settings.yml
  40. 1
      modules/islandora_core_feature/config/install/views.view.all_taxonomy_terms.yml
  41. 237
      modules/islandora_core_feature/config/install/views.view.file_checksum.yml
  42. 182
      modules/islandora_core_feature/config/install/views.view.manage_members.yml
  43. 1
      modules/islandora_core_feature/config/install/views.view.non_fedora_files.yml
  44. 3
      modules/islandora_core_feature/islandora_core_feature.info.yml
  45. 20
      modules/islandora_core_feature/islandora_core_feature.post_update.php
  46. 10
      modules/islandora_iiif/README.md
  47. 11
      modules/islandora_iiif/config/schema/islandora_iiif.schema.yml
  48. 3
      modules/islandora_iiif/islandora_iiif.info.yml
  49. 12
      modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php
  50. 260
      modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php
  51. 3
      modules/islandora_image/islandora_image.info.yml
  52. 31
      modules/islandora_image/src/Plugin/Action/GenerateImageDerivativeFile.php
  53. 5
      modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php
  54. 3
      modules/islandora_text_extraction/islandora_text_extraction.info.yml
  55. 4
      modules/islandora_text_extraction/islandora_text_extraction.module
  56. 24
      modules/islandora_text_extraction/src/Controller/MediaSourceController.php
  57. 14
      modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php
  58. 31
      modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivativeFile.php
  59. 5
      modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php
  60. 4
      modules/islandora_text_extraction/tests/src/Functional/LoadTest.php
  61. 3
      modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml
  62. 3
      modules/islandora_video/config/schema/islandora_video.schema.yml
  63. 3
      modules/islandora_video/islandora_video.info.yml
  64. 2
      modules/islandora_video/templates/islandora-file-video.html.twig
  65. 5
      modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php
  66. 2
      phpunit.xml
  67. 4
      src/Commands/IslandoraCommands.php
  68. 28
      src/Controller/ManageMediaController.php
  69. 21
      src/Controller/ManageMembersController.php
  70. 3
      src/Controller/MediaSourceController.php
  71. 2
      src/Event/StompHeaderEvent.php
  72. 59
      src/EventGenerator/EmitEvent.php
  73. 16
      src/EventGenerator/EventGenerator.php
  74. 6
      src/EventSubscriber/LinkHeaderSubscriber.php
  75. 4
      src/EventSubscriber/MediaLinkHeaderSubscriber.php
  76. 6
      src/EventSubscriber/NodeLinkHeaderSubscriber.php
  77. 11
      src/Exception/IslandoraDerivativeException.php
  78. 51
      src/Flysystem/Adapter/FedoraAdapter.php
  79. 34
      src/Flysystem/Fedora.php
  80. 2
      src/Form/AddChildrenForm.php
  81. 258
      src/Form/AddChildrenWizard/AbstractBatchProcessor.php
  82. 157
      src/Form/AddChildrenWizard/AbstractFileSelectionForm.php
  83. 125
      src/Form/AddChildrenWizard/AbstractForm.php
  84. 71
      src/Form/AddChildrenWizard/Access.php
  85. 57
      src/Form/AddChildrenWizard/ChildBatchProcessor.php
  86. 32
      src/Form/AddChildrenWizard/ChildFileSelectionForm.php
  87. 24
      src/Form/AddChildrenWizard/ChildForm.php
  88. 157
      src/Form/AddChildrenWizard/ChildTypeSelectionForm.php
  89. 66
      src/Form/AddChildrenWizard/FieldTrait.php
  90. 34
      src/Form/AddChildrenWizard/MediaBatchProcessor.php
  91. 32
      src/Form/AddChildrenWizard/MediaFileSelectionForm.php
  92. 24
      src/Form/AddChildrenWizard/MediaForm.php
  93. 227
      src/Form/AddChildrenWizard/MediaTypeSelectionForm.php
  94. 58
      src/Form/AddChildrenWizard/MediaTypeTrait.php
  95. 40
      src/Form/AddChildrenWizard/WizardTrait.php
  96. 10
      src/Form/ConfirmDeleteMediaAndFile.php
  97. 98
      src/Form/IslandoraSettingsForm.php
  98. 32
      src/IslandoraContextManager.php
  99. 83
      src/IslandoraUtils.php
  100. 22
      src/MediaSource/MediaSourceService.php
  101. Some files were not shown because too many files have changed in this diff Show More

2
.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

23
.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 }}"

26
.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 }}

35
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.

37
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",

2
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: []

91
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

3
css/islandora.css

@ -0,0 +1,3 @@
.container .islandora-media-items {
margin: 0;
}

43
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

52
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.');
}
}

5
islandora.libraries.yml

@ -0,0 +1,5 @@
islandora:
version: VERSION
css:
theme:
css/islandora.css: {}

241
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;

16
islandora.post_update.php

@ -0,0 +1,16 @@
<?php
/**
* @file
* Post updates.
*/
/**
* Set default value for delete_media_and_files field in settings.
*/
function islandora_post_update_delete_media_and_files() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('islandora.settings');
$config->set('delete_media_and_files', TRUE);
$config->save(TRUE);
}

15
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'

21
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'

35
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 '';
}

51
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',
],
];
}

4
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

102
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 <li> 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
);
}
}
}

147
modules/islandora_advanced_search/js/facets/facets-views-ajax.js

@ -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);

70
modules/islandora_advanced_search/js/facets/soft-limit.js

@ -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);

2
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')
);
}

2
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()
);
}

2
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()
);
}

3
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

3
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

5
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',

9
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

9
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'

5
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

18
modules/islandora_breadcrumbs/islandora_breadcrumbs.install

@ -0,0 +1,18 @@
<?php
/**
* @file
* Install/update hook implementations.
*/
/**
* Update referenceField config to referenceFields.
*/
function islandora_breadcrumbs_update_8001() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('islandora_breadcrumbs.breadcrumbs');
$config->set('referenceFields', [$config->get('referenceField')]);
$config->clear('referenceField');
$config->save();
return "Updated referenceFields config.";
}

5
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'

7
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'

2
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 }

132
modules/islandora_breadcrumbs/src/Form/IslandoraBreadcrumbsSettingsForm.php

@ -0,0 +1,132 @@
<?php
namespace Drupal\islandora_breadcrumbs\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure islandora_breadcrumbs settings.
*/
class IslandoraBreadcrumbsSettingsForm extends ConfigFormBase {
/**
* Config settings.
*
* @var string
*/
const SETTINGS = 'islandora_breadcrumbs.breadcrumbs';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_breadcrumbs_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
static::SETTINGS,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->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.<br>One per line.<br>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);
}
}

69
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'), '<front>'));
$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);
}
}
}

4
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.

4
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

23
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: { }

1
modules/islandora_core_feature/config/install/views.view.all_taxonomy_terms.yml

@ -168,4 +168,3 @@ display:
- url.query_args
- user.permissions
tags: { }

237
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: { }

182
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

1
modules/islandora_core_feature/config/install/views.view.non_fedora_files.yml

@ -194,4 +194,3 @@ display:
- url.query_args
- user.permissions
tags: { }

3
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

20
modules/islandora_core_feature/islandora_core_feature.post_update.php

@ -0,0 +1,20 @@
<?php
/**
* @file
* Post-update hooks.
*/
/**
* Add index to field_weight.
*/
function islandora_core_feature_post_update_add_index_to_field_weight() {
$storage = \Drupal::entityTypeManager()->getStorage('field_storage_config');
$field = $storage->load('node.field_weight');
$indexes = $field->getIndexes();
$indexes += [
'value' => ['value'],
];
$field->setIndexes($indexes);
$field->save();
}

10
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/).

11
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

3
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

12
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();
}

260
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,
];
}
/**

3
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

31
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;
}

5
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',

3
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

4
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) == '<?xml') {
return;
}
$data = nl2br($data);
$media->set('field_edited_text', $data);
$media->field_edited_text->format = 'basic_html';

24
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()}");

14
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:<br /><code>-c tessedit_create_hocr=1 -c hocr_font_info=0</code>");
return $form;
}

31
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()

5
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;

4
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);

3
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

3
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

3
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

2
modules/islandora_video/templates/islandora-file-video.html.twig

@ -21,7 +21,7 @@
{% endfor %}
{% if tracks %}
{% for track in tracks %}
<track {{ track.track_attributes }}
<track {{ track.track_attributes }} />
{% endfor %}
{% endif %}
</video>

5
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',

2
phpunit.xml

@ -58,7 +58,7 @@
</testsuite>
<testsuite name="functional">
<directory>../modules/contrib/islandora/tests/src/Functional</directory>
<directory>../modules/contrib/isladnora/modules/*/tests/src/Functional</directory>
<directory>../modules/contrib/islandora/modules/*/tests/src/Functional</directory>
</testsuite>
<testsuite name="functional-javascript">
<directory>../modules/contrib/islandora/tests/src/FunctionalJavascript</directory>

4
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;

28
src/Controller/ManageMediaController.php

@ -6,6 +6,7 @@ use Drupal\islandora\IslandoraUtils;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\RouteMatch;
use Drupal\node\Entity\Node;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
/**
@ -25,7 +26,7 @@ class ManageMediaController extends ManageMembersController {
public function addToNodePage(NodeInterface $node) {
$field = IslandoraUtils::MEDIA_OF_FIELD;
return $this->generateTypeList(
$add_media_list = $this->generateTypeList(
'media',
'media_type',
'entity.media.add_form',
@ -33,6 +34,21 @@ class ManageMediaController extends ManageMembersController {
$field,
['query' => ["edit[$field][widget][0][target_id]" => $node->id()]]
);
$manage_link = Url::fromRoute('entity.media_type.collection')->toRenderArray();
$manage_link['#title'] = $this->t('Manage media types');
$manage_link['#type'] = 'link';
$manage_link['#prefix'] = ' ';
$manage_link['#suffix'] = '.';
return [
'#type' => 'markup',
'#markup' => $this->t("The following media types can be added because they have the <code>@field</code> field.", [
'@field' => $field,
]),
'manage_link' => $manage_link,
'add_media' => $add_media_list,
];
}
/**
@ -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();

21
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 <code>@field</code> field.", [
'@field' => $field,
]),
'manage_link' => $manage_link,
'add_node' => $add_node_list,
];
}
/**

3
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));
}
}

2
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.

59
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'
)
);
}
}

16
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();

6
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);
}

4
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');

6
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');

11
src/Exception/IslandoraDerivativeException.php

@ -0,0 +1,11 @@
<?php
namespace Drupal\islandora\Exception;
/**
* Islandora exceptions.
*
* @package islandora
*/
class IslandoraDerivativeException extends \RuntimeException {
}

51
src/Flysystem/Adapter/FedoraAdapter.php

@ -2,6 +2,8 @@
namespace Drupal\islandora\Flysystem\Adapter;
use GuzzleHttp\Psr7\Header;
use Drupal\Core\Logger\LoggerChannelInterface;
use Islandora\Chullo\IFedoraApi;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Adapter\Polyfill\NotSupportingVisibilityTrait;
@ -9,7 +11,7 @@ use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
use League\Flysystem\Config;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\StreamWrapper;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
/**
* Fedora adapter for Flysystem.
@ -29,21 +31,35 @@ class FedoraAdapter implements AdapterInterface {
/**
* Mimetype guesser.
*
* @var \Symfony\Component\HttpFoundation\File\Mimetype\MimeTypeGuesserInterface
* @var \Symfony\Component\Mime\MimeTypeGuesserInterface
*/
protected $mimeTypeGuesser;
/**
* Logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Constructs a Fedora adapter 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\Logger\LoggerChannelInterface $logger
* The fedora adapter logger channel.
*/
public function __construct(IFedoraApi $fedora, MimeTypeGuesserInterface $mime_type_guesser) {
public function __construct(
IFedoraApi $fedora,
MimeTypeGuesserInterface $mime_type_guesser,
LoggerChannelInterface $logger
) {
$this->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] == '<http://www.w3.org/ns/ldp#NonRDFSource>') {
$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');

34
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);
}
/**

2
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) {

258
src/Form/AddChildrenWizard/AbstractBatchProcessor.php

@ -0,0 +1,258 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\islandora\IslandoraUtils;
use Drupal\media\MediaInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
/**
* Abstract addition batch processor.
*/
abstract class AbstractBatchProcessor {
use FieldTrait;
use DependencySerializationTrait;
use StringTranslationTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected ?EntityTypeManagerInterface $entityTypeManager = NULL;
/**
* The database connection serivce.
*
* @var \Drupal\Core\Database\Connection|null
*/
protected ?Connection $database;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface|null
*/
protected ?AccountProxyInterface $currentUser;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected MessengerInterface $messenger;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected DateFormatterInterface $dateFormatter;
/**
* Constructor.
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
Connection $database,
AccountProxyInterface $current_user,
MessengerInterface $messenger,
DateFormatterInterface $date_formatter
) {
$this->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 <a target="_blank" href=":uri">bulk created entity of type %type, with ID %id</a>.',
'@count validation errors present in <a target="_blank" href=":uri">bulk created entity of type %type, with ID %id</a>.',
[
'%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.'));
}
}
}

157
src/Form/AddChildrenWizard/AbstractFileSelectionForm.php

@ -0,0 +1,157 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\media\MediaTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Children addition wizard's second step.
*/
abstract class AbstractFileSelectionForm extends FormBase {
use WizardTrait;
const BATCH_PROCESSOR = 'abstract.abstract';
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface|null
*/
protected ?AccountProxyInterface $currentUser;
/**
* The batch processor service.
*
* @var \Drupal\islandora\Form\AddChildrenWizard\AbstractBatchProcessor|null
*/
protected ?AbstractBatchProcessor $batchProcessor;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
$instance = parent::create($container);
$instance->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());
}
}

125
src/Form/AddChildrenWizard/AbstractForm.php

@ -0,0 +1,125 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\ctools\Wizard\FormWizardBase;
use Drupal\islandora\IslandoraUtils;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Bulk children addition wizard base form.
*/
abstract class AbstractForm extends FormWizardBase {
const TEMPSTORE_ID = 'abstract.abstract';
const TYPE_SELECTION_FORM = MediaTypeSelectionForm::class;
const FILE_SELECTION_FORM = AbstractFileSelectionForm::class;
/**
* The Islandora Utils service.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected IslandoraUtils $utils;
/**
* The current node ID.
*
* @var mixed|null
*/
protected $nodeId;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected RouteMatchInterface $currentRoute;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected AccountProxyInterface $currentUser;
/**
* Constructor.
*/
public function __construct(
SharedTempStoreFactory $tempstore,
FormBuilderInterface $builder,
ClassResolverInterface $class_resolver,
EventDispatcherInterface $event_dispatcher,
RouteMatchInterface $route_match,
RendererInterface $renderer,
$tempstore_id,
AccountProxyInterface $current_user,
$machine_name = NULL,
$step = NULL
) {
parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $renderer, $tempstore_id,
$machine_name, $step);
$this->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];
}
}

71
src/Form/AddChildrenWizard/Access.php

@ -0,0 +1,71 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\islandora\IslandoraUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Access checker.
*
* The _wizard/_form route enhancers do not really allow for access checking
* things, so let's roll it separately for now.
*/
class Access implements ContainerInjectionInterface {
/**
* The Islandora utils service.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected IslandoraUtils $utils;
/**
* Constructor.
*/
public function __construct(IslandoraUtils $utils) {
$this->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'));
}
}

57
src/Form/AddChildrenWizard/ChildBatchProcessor.php

@ -0,0 +1,57 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\islandora\IslandoraUtils;
use Drupal\node\NodeInterface;
/**
* Children addition batch processor.
*/
class ChildBatchProcessor extends AbstractBatchProcessor {
/**
* {@inheritdoc}
*/
protected function getNode($info, array $values) : NodeInterface {
$taxonomy_term_storage = $this->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);
}
}

32
src/Form/AddChildrenWizard/ChildFileSelectionForm.php

@ -0,0 +1,32 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Children addition wizard's second step.
*/
class ChildFileSelectionForm extends AbstractFileSelectionForm {
public const BATCH_PROCESSOR = 'islandora.upload_children.batch_processor';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_add_children_wizard_file_selection';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$cached_values = $form_state->getTemporaryValue('wizard');
$form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members"));
}
}

24
src/Form/AddChildrenWizard/ChildForm.php

@ -0,0 +1,24 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
/**
* Bulk children addition wizard base form.
*/
class ChildForm extends AbstractForm {
const TEMPSTORE_ID = 'islandora.upload_children';
const TYPE_SELECTION_FORM = ChildTypeSelectionForm::class;
const FILE_SELECTION_FORM = ChildFileSelectionForm::class;
/**
* {@inheritdoc}
*/
public function getMachineName() {
return strtr("islandora_add_children_wizard__{userid}__{nodeid}", [
'{userid}' => $this->currentUser->id(),
'{nodeid}' => $this->nodeId,
]);
}
}

157
src/Form/AddChildrenWizard/ChildTypeSelectionForm.php

@ -0,0 +1,157 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Form\FormStateInterface;
use Drupal\islandora\IslandoraUtils;
/**
* Children addition wizard's first step.
*/
class ChildTypeSelectionForm extends MediaTypeSelectionForm {
/**
* {@inheritdoc}
*/
public function getFormId() : string {
return 'islandora_add_children_type_selection';
}
/**
* Memoization for ::getNodeBundleOptions().
*
* @var array|null
*/
protected ?array $nodeBundleOptions = NULL;
/**
* Indicate presence of model field on node bundles.
*
* Populated as a side effect of ::getNodeBundleOptions().
*
* @var array|null
*/
protected ?array $nodeBundleHasModelField = NULL;
/**
* Helper; get the node bundle options available to the current user.
*
* @return array
* An associative array mapping node bundle machine names to their human-
* readable labels.
*/
protected function getNodeBundleOptions() : array {
if ($this->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',
]
);
}
}

66
src/Form/AddChildrenWizard/FieldTrait.php

@ -0,0 +1,66 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
* Field lookup helper trait.
*/
trait FieldTrait {
use MediaTypeTrait;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface|null
*/
protected ?EntityFieldManagerInterface $entityFieldManager = NULL;
/**
* Helper; get field instance, given our required values.
*
* @param array $values
* See ::getMediaType() for which values are required.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The target field.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getField(array $values): FieldDefinitionInterface {
$media_type = $this->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;
}
}

34
src/Form/AddChildrenWizard/MediaBatchProcessor.php

@ -0,0 +1,34 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\node\NodeInterface;
/**
* Media addition batch processor.
*/
class MediaBatchProcessor extends AbstractBatchProcessor {
/**
* {@inheritdoc}
*/
protected function getNode($info, array $values) : NodeInterface {
return $this->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);
}
}

32
src/Form/AddChildrenWizard/MediaFileSelectionForm.php

@ -0,0 +1,32 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Media addition wizard's second step.
*/
class MediaFileSelectionForm extends AbstractFileSelectionForm {
public const BATCH_PROCESSOR = 'islandora.upload_media.batch_processor';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_add_media_wizard_file_selection';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$cached_values = $form_state->getTemporaryValue('wizard');
$form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/media"));
}
}

24
src/Form/AddChildrenWizard/MediaForm.php

@ -0,0 +1,24 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
/**
* Bulk children addition wizard base form.
*/
class MediaForm extends AbstractForm {
const TEMPSTORE_ID = 'islandora.upload_media';
const TYPE_SELECTION_FORM = MediaTypeSelectionForm::class;
const FILE_SELECTION_FORM = MediaFileSelectionForm::class;
/**
* {@inheritdoc}
*/
public function getMachineName() {
return strtr("islandora_add_media_wizard__{userid}__{nodeid}", [
'{userid}' => $this->currentUser->id(),
'{nodeid}' => $this->nodeId,
]);
}
}

227
src/Form/AddChildrenWizard/MediaTypeSelectionForm.php

@ -0,0 +1,227 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\islandora\IslandoraUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Children addition wizard's first step.
*/
class MediaTypeSelectionForm extends FormBase {
/**
* Cacheable metadata that is instantiated and used internally.
*
* @var \Drupal\Core\Cache\CacheableMetadata|null
*/
protected ?CacheableMetadata $cacheableMetadata = NULL;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|null
*/
protected ?EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected ?EntityTypeManagerInterface $entityTypeManager;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface|null
*/
protected ?EntityFieldManagerInterface $entityFieldManager;
/**
* The Islandora Utils service.
*
* @var \Drupal\islandora\IslandoraUtils|null
*/
protected ?IslandoraUtils $utils;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) : self {
$instance = parent::create($container);
$instance->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 <a target="_blank" href=":url">Portland Common Data Model: Use Extension</a>. "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);
}
}

58
src/Form/AddChildrenWizard/MediaTypeTrait.php

@ -0,0 +1,58 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\media\MediaTypeInterface;
/**
* Media type lookup helper trait.
*/
trait MediaTypeTrait {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
*/
protected ?EntityTypeManagerInterface $entityTypeManager = NULL;
/**
* Helper; get media type, given our required values.
*
* @param array $values
* An associative array which must contain at least:
* - media_type: The machine name of the media type to load.
*
* @return \Drupal\media\MediaTypeInterface
* The loaded media type.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getMediaType(array $values): MediaTypeInterface {
return $this->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;
}
}

40
src/Form/AddChildrenWizard/WizardTrait.php

@ -0,0 +1,40 @@
<?php
namespace Drupal\islandora\Form\AddChildrenWizard;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\WidgetInterface;
/**
* Wizard/widget lookup helper trait.
*/
trait WizardTrait {
use FieldTrait;
/**
* The widget plugin manager service.
*
* @var \Drupal\Core\Field\WidgetPluginManager
*/
protected PluginManagerInterface $widgetPluginManager;
/**
* Helper; get the base widget for the given field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* The field for which get obtain the widget.
*
* @return \Drupal\Core\Field\WidgetInterface
* The widget.
*/
protected function getWidget(FieldDefinitionInterface $field): WidgetInterface {
return $this->widgetPluginManager->getInstance([
'field_definition' => $field,
'form_mode' => 'default',
'prepare' => TRUE,
]);
}
}

10
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++;
}
}
}
}

98
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"]);
}
}
}

32
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;
}
}

83
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;
}
}

22
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();

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save