diff --git a/.travis.yml b/.travis.yml index 80dbcb06..685620f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: php php: - 7.1 - 7.2 +services: + - mysql matrix: fast_finish: true @@ -24,12 +26,14 @@ install: - COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=-1 $COMPOSER_PATH config repositories.local path "$TRAVIS_BUILD_DIR" - COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=-1 $COMPOSER_PATH require "islandora/islandora:dev-travis-testing as dev-8.x-1.x" --prefer-source --update-with-dependencies - cd web; drush --uri=127.0.0.1:8282 en -y islandora + - (drush -y --uri=127.0.0.1:8282 en islandora_core_feature; drush -y --uri=127.0.0.1:8282 fim islandora_core_feature) + - drush -y --uri=127.0.0.1:8282 en islandora_audio islandora_breadcrumbs islandora_iiif islandora_image islandora_video + - (drush -y --uri=127.0.0.1:8282 en islandora_text_extraction_defaults; drush -y --uri=127.0.0.1:8282 fim islandora_text_extraction_defaults) script: - - $SCRIPT_DIR/line_endings.sh $TRAVIS_BUILD_DIR - - phpcs --standard=Drupal --ignore=*.md --extensions=php,module,inc,install,test,profile,theme,css,info $TRAVIS_BUILD_DIR - - phpcpd --names *.module,*.inc,*.test,*.php $TRAVIS_BUILD_DIR - - php core/scripts/run-tests.sh --suppress-deprecations --url http://127.0.0.1:8282 --verbose --php `which php` --module "islandora" + - $SCRIPT_DIR/travis_scripts.sh + - $SCRIPT_DIR/run-tests.sh "islandora" + - $SCRIPT_DIR/run-tests.sh "islandora_breadcrumbs" notifications: irc: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c5dd0d4..916d6794 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,10 @@ # Welcome! -If you are reading this document then you are interested in contributing to the Islandora CLAW. All contributions are welcome: use-cases, documentation, code, patches, bug reports, feature requests, etc. You do not need to be a programmer to speak up! +If you are reading this document then you are interested in contributing to Islandora 8. All contributions are welcome: use-cases, documentation, code, patches, bug reports, feature requests, etc. You do not need to be a programmer to speak up! + +We also have an irc channel -- #islandora -- on freenode.net. Feel free to hang out there, ask questions, and help others out if you can. + +Please note that this project operates under the [Islandora Community Code of Conduct](http://islandora.ca/codeofconduct). By participating in this project you agree to abide by its terms. ## Workflows @@ -8,7 +12,7 @@ The group meets each Wednesday at 1:00 PM Eastern. Meeting notes and announcemen ### Use cases -If you would like to submit a use case to the Islandora CLAW project, please submit an issue [here](https://github.com/Islandora-CLAW/CLAW/issues/new) using the [Use Case template](https://github.com/Islandora-CLAW/CLAW/wiki/Use-Case-template), prepending "Use Case:" to the title of the issue. +If you would like to submit a use case to the Islandora 8 project, please submit an issue [here](https://github.com/Islandora-CLAW/CLAW/issues/new) using the [Use Case template](https://github.com/Islandora-CLAW/CLAW/wiki/Use-Case-template), prepending "Use Case:" to the title of the issue. ### Documentation @@ -16,11 +20,11 @@ You can contribute documentation in two different ways. One way is to create an ### Request a new feature -To request a new feature you should [open an issue in the CLAW repository](https://github.com/Islandora-CLAW/CLAW/issues/new) or create a use case (see the _Use cases_ section above), and summarize the desired functionality. Prepend "Enhancement:" if creating an issue on the project repo, and "Use Case:" if creating a use case. +To request a new feature you should [open an issue in the Islandora 8 repository](https://github.com/Islandora-CLAW/CLAW/issues/new) or create a use case (see the _Use cases_ section above), and summarize the desired functionality. Prepend "Enhancement:" if creating an issue on the project repo, and "Use Case:" if creating a use case. ### Report a bug -To report a bug you should [open an issue in the CLAW repository](https://github.com/Islandora-CLAW/CLAW/issues/new) that summarizes the bug. Prepend the label "Bug:" to the title of the issue. +To report a bug you should [open an issue in the Islandora 8 repository](https://github.com/Islandora-CLAW/CLAW/issues/new) that summarizes the bug. Prepend the label "Bug:" to the title of the issue. In order to help us understand and fix the bug it would be great if you could provide us with: @@ -36,7 +40,7 @@ That is great! In this case please send us a pull request as described in the se ### Contribute code -Before you set out to contribute code you will need to have completed a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor Licencse Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to +Before you set out to contribute code you will need to have completed a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to _If you are interested in contributing code to Islandora but do not know where to begin:_ @@ -66,4 +70,4 @@ You may want to read [Syncing a fork](https://help.github.com/articles/syncing-a ## License Agreements -The Islandora Foundation requires that contributors complete a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to community@islandora.ca. This license is for your protection as a contributor as well as the protection of the Foundation and its users; it does not change your rights to use your own contributions for any other purpose. +The Islandora Foundation requires that contributors complete a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to community@islandora.ca. This license is for your protection as a contributor as well as the protection of the Foundation and its users; it does not change your rights to use your own contributions for any other purpose. A list of current CLAs is kept [here](https://github.com/Islandora/islandora/wiki/Contributor-License-Agreements). diff --git a/composer.json b/composer.json index 1923cff9..41d76a2f 100644 --- a/composer.json +++ b/composer.json @@ -21,15 +21,15 @@ "stomp-php/stomp-php": "4.*", "drupal/jwt": "1.0.0-alpha6", "drupal/filehash": "^1.1", - "drupal/prepopulate" : "^2.0@alpha", + "drupal/prepopulate" : "^2.2", "drupal/eva" : "^1.3", "drupal/features" : "^3.7", - "drupal/migrate_plus" : "4.0-beta3", - "drupal/migrate_tools" : "4.0-beta3", + "drupal/migrate_plus" : "^4.1", + "drupal/migrate_tools" : "^4.1", "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "islandora/chullo" : "dev-master" + "islandora/crayfish-commons": "dev-dev" }, "require-dev": { "phpunit/phpunit": "^6", diff --git a/config/install/islandora.settings.yml b/config/install/islandora.settings.yml new file mode 100644 index 00000000..8fb25fb8 --- /dev/null +++ b/config/install/islandora.settings.yml @@ -0,0 +1,4 @@ +broker_url: 'tcp://localhost:61613' +jwt_expiry: '+2 hour' +gemini_url: '' +gemini_pseudo_bundles: [] diff --git a/config/install/system.action.delete_media.yml b/config/install/system.action.delete_media.yml deleted file mode 100644 index c95e8423..00000000 --- a/config/install/system.action.delete_media.yml +++ /dev/null @@ -1,13 +0,0 @@ -langcode: en -status: true -dependencies: - enforced: - module: - - islandora - module: - - islandora -id: delete_media -label: 'Delete media' -type: media -plugin: delete_media -configuration: { } diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index ad59a82c..8fd719ce 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -3,14 +3,26 @@ islandora.settings: label: 'Islandora Core Settings' mapping: broker_url: - type: string + type: string label: 'Url to connect to message broker' fedora_rest_endpoint: - type: string + type: string label: 'Url to Fedora instance' broadcast_queue: - type: string + type: string label: 'Queue that handles distributing messages amongst multiple recipients' + jwt_expiry: + type: string + label: 'How long JWTs should last before expiring.' + 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' + sequence: + type: string + action.configuration.emit_node_event: type: mapping @@ -56,10 +68,6 @@ action.configuration.emit_term_event: type: text label: 'Event Type' -action.configuration.delete_media: - type: action_configuration_default - label: 'Delete media' - action.configuration.delete_media_and_file: type: action_configuration_default label: 'Delete media and file' @@ -68,21 +76,31 @@ condition.plugin.node_has_term: type: condition.plugin mapping: uri: - type: text + type: text label: 'Taxonomy Term URI' +condition.plugin.node_has_parent: + type: condition.plugin + mapping: + parent_nid: + type: integer + label: 'Parent node' + parent_reference_field: + type: string + label: 'Parent reference field' + condition.plugin.media_has_term: type: condition.plugin mapping: uri: - type: text + type: text label: 'Taxonomy Term URI' condition.plugin.parent_node_has_term: type: condition.plugin mapping: uri: - type: text + type: text label: 'Taxonomy Term URI' condition.plugin.file_uses_filesystem: @@ -101,6 +119,13 @@ condition.plugin.media_uses_filesystem: sequence: type: string +condition.plugin.media_has_mimetype: + type: condition.plugin + mapping: + mimetypes: + type: text + label: 'Mime types' + condition.plugin.content_entity_type: type: condition.plugin mapping: @@ -108,3 +133,24 @@ condition.plugin.content_entity_type: type: sequence sequence: type: string + +condition.plugin.node_had_namespace: + type: condition.plugin + mapping: + namespace: + type: text + label: 'Namespace' + pid_field: + type: ignore + label: 'PID field' + +field.formatter.settings.islandora_image: + type: mapping + label: 'Image field display format settings' + mapping: + image_link: + type: string + label: 'Link image to' + image_style: + type: string + label: 'Image style' diff --git a/drush.services.yml b/drush.services.yml new file mode 100644 index 00000000..fb36bda7 --- /dev/null +++ b/drush.services.yml @@ -0,0 +1,5 @@ +services: + islandora.commands: + class: \Drupal\islandora\Commands\IslandoraCommands + tags: + - { name: drush.command } diff --git a/islandora.info.yml b/islandora.info.yml index 0c0dbf0f..0e0a265e 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -3,7 +3,7 @@ name: 'islandora' description: "Islandora Core" type: module -package: islandora +package: Islandora core: 8.x dependencies: - block diff --git a/islandora.install b/islandora.install index e900f2f6..526583d4 100644 --- a/islandora.install +++ b/islandora.install @@ -40,3 +40,15 @@ function islandora_schema() { ]; return $schema; } + +/** + * Delete the 'delete_media' action we used to provide, if it exists. + * + * Use the core 'media_delete_action' instead. + */ +function islandora_update_8001(&$sandbox) { + $action = \Drupal::service('entity_type.manager')->getStorage('action')->load('delete_media'); + if ($action) { + $action->delete(); + } +} diff --git a/islandora.links.action.yml b/islandora.links.action.yml index 97b16e10..f8bf7d2b 100644 --- a/islandora.links.action.yml +++ b/islandora.links.action.yml @@ -6,6 +6,12 @@ islandora.add_media_to_node: islandora.add_member_to_node: route_name: islandora.add_member_to_node_page - title: Add member + title: Add child + appears_on: + - view.manage_members.page_1 + +islandora.reorder_children: + route_name: view.reorder_children.page_1 + title: Reorder Children appears_on: - view.manage_members.page_1 diff --git a/islandora.module b/islandora.module index 3e06b0af..dda56c8c 100644 --- a/islandora.module +++ b/islandora.module @@ -14,8 +14,12 @@ * @author Diego Pino Navarro https://github.com/diegopino */ +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; +use Drupal\islandora\Form\IslandoraSettingsForm; +use Drupal\islandora\GeminiLookup; use Drupal\node\NodeInterface; use Drupal\media\MediaInterface; use Drupal\file\FileInterface; @@ -56,6 +60,8 @@ function islandora_rdf_namespaces() { 'pcdm' => 'http://pcdm.org/models#', 'use' => 'http://pcdm.org/use#', 'iana' => 'http://www.iana.org/assignments/relation/', + 'premis' => 'http://www.loc.gov/premis/rdf/v1#', + 'co' => 'http://purl.org/co/', ]; } @@ -257,23 +263,27 @@ function islandora_entity_view_mode_alter(&$view_mode, EntityInterface $entity) // ContextReaction. $storage = \Drupal::service('entity_type.manager')->getStorage('entity_view_mode'); $context_manager = \Drupal::service('context.manager'); - - foreach ($context_manager->getActiveReactions('\Drupal\islandora\Plugin\ContextReaction\ViewModeAlterReaction') as $reaction) { - // Construct the new view mode's machine name. - $entity_type = $entity->getEntityTypeId(); - $mode = $reaction->execute(); - $machine_name = "$entity_type.$mode"; - - // Try to load it. - $new_mode = $storage->load($machine_name); - - // If successful, alter the view mode. - if ($new_mode) { - $view_mode = $mode; - } - else { - // Otherwise, leave it be, but log a message. - \Drupal::logger('islandora')->info("EntityViewMode $machine_name does not exist. View mode cannot be altered."); + $current_entity = \Drupal::routeMatch()->getParameter('node'); + $current_id = ($current_entity instanceof NodeInterface) ? $current_entity->id() : NULL; + if (isset($current_id) && $current_id == $entity->id()) { + foreach ($context_manager->getActiveReactions('\Drupal\islandora\Plugin\ContextReaction\ViewModeAlterReaction') as $reaction) { + // Construct the new view mode's machine name. + $entity_type = $entity->getEntityTypeId(); + $mode = $reaction->execute(); + $machine_name = "$entity_type.$mode"; + + // Try to load it. + $new_mode = $storage->load($machine_name); + + // If successful, alter the view mode. + if ($new_mode) { + $view_mode = $mode; + } + else { + // Otherwise, leave it be, but log a message. + \Drupal::logger('islandora') + ->info("EntityViewMode $machine_name does not exist. View mode cannot be altered."); + } } } } @@ -335,8 +345,111 @@ function islandora_form_block_form_alter(&$form, FormStateInterface $form_state, // to alter block layout. unset($form['visibility']['content_entity_type']); unset($form['visibility']['parent_node_has_term']); + unset($form['visibility']['node_had_namespace']); unset($form['visibility']['media_has_term']); unset($form['visibility']['file_uses_filesystem']); unset($form['visibility']['node_has_term']); + unset($form['visibility']['node_has_parent']); unset($form['visibility']['media_uses_filesystem']); + unset($form['visibility']['media_has_mimetype']); +} + +/** + * Implements hook_entity_extra_field_info(). + */ +function islandora_entity_extra_field_info() { + $config_factory = \Drupal::service('config.factory')->get(IslandoraSettingsForm::CONFIG_NAME); + $extra_field = []; + + $pseudo_bundles = $config_factory->get(IslandoraSettingsForm::GEMINI_PSEUDO); + + foreach ($pseudo_bundles as $key) { + list($bundle, $content_entity) = explode(":", $key); + $extra_field[$content_entity][$bundle]['display']['field_gemini_uri'] = [ + 'label' => t('Fedora URI'), + 'description' => t('The URI to the persistent'), + 'weight' => 100, + 'visible' => TRUE, + ]; + } + return $extra_field; +} + +/** + * Implements hook_entity_view(). + */ +function islandora_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { + $route_match_item = \Drupal::routeMatch()->getParameters()->all(); + // Get the parameter, which might be node, media or taxonomy term. + $current_entity = reset($route_match_item); + // Match exactly to ensure they are the same entity type too. + if ($entity === $current_entity) { + if ($display->getComponent('field_gemini_uri')) { + $gemini = \Drupal::service('islandora.gemini.lookup'); + if ($gemini instanceof GeminiLookup) { + $fedora_uri = $gemini->lookup($entity); + if (!is_null($fedora_uri)) { + $build['field_gemini_uri'] = [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'field-gemini-uri', + ], + 'internal_label' => [ + '#type' => 'item', + '#title' => t('Fedora URI'), + 'internal_uri' => [ + '#type' => 'link', + '#title' => t("@url", ['@url' => $fedora_uri]), + '#url' => Url::fromUri($fedora_uri), + ], + ], + ]; + } + } + } + } +} + +/** + * Implements hook_preprocess_views_view_table(). + * + * Used for the integer-weight drag-n-drop. Taken almost + * verbatim from the weight module. + */ +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') { + + // 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; + + // Add the tabledrag attributes. + foreach ($variables['rows'] as $key => $row) { + if ($is_first_column) { + // If the weight selector is the first column move it to the last + // column, in order to make the draggable widget appear. + $weight_selector = $variables['rows'][$key]['columns'][$field->field]; + unset($variables['rows'][$key]['columns'][$field->field]); + $variables['rows'][$key]['columns'][$field->field] = $weight_selector; + } + // Add draggable attribute. + $variables['rows'][$key]['attributes']->addClass('draggable'); + } + // The row key identify in an unique way a view grouped by a field. + // Without row number, all the groups will share the same table_id + // and just the first table can be draggable. + $table_id = 'weight-table-' . $variables['view']->dom_id . '-row-' . $key; + $variables['attributes']['id'] = $table_id; + + $options = [ + 'table_id' => $table_id, + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'weight-selector', + ]; + drupal_attach_tabledrag($variables, $options); + } + } } diff --git a/islandora.services.yml b/islandora.services.yml index 6f6b85c7..b08e0699 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -3,6 +3,7 @@ services: islandora.eventgenerator: class: Drupal\islandora\EventGenerator\EventGenerator + arguments: ['@islandora.utils', '@islandora.media_source_service'] islandora.stomp: class: Stomp\StatefulStomp factory: ['Drupal\islandora\StompFactory', create] @@ -15,7 +16,7 @@ services: - { name: event_subscriber } islandora.media_link_header_subscriber: class: Drupal\islandora\EventSubscriber\MediaLinkHeaderSubscriber - arguments: ['@entity_type.manager', '@entity_field.manager', '@access_manager', '@current_user', '@current_route_match', '@request_stack'] + arguments: ['@entity_type.manager', '@entity_field.manager', '@access_manager', '@current_user', '@current_route_match', '@request_stack', '@islandora.utils'] tags: - { name: event_subscriber } islandora.node_link_header_subscriber: @@ -50,4 +51,11 @@ services: arguments: ['@entity_type.manager', '@current_user', '@language_manager', '@entity.query', '@file_system', '@islandora.utils'] islandora.utils: class: Drupal\islandora\IslandoraUtils - arguments: ['@entity_type.manager', '@entity_field.manager', '@entity.query', '@context.manager', '@flysystem_factory'] + arguments: ['@entity_type.manager', '@entity_field.manager', '@entity.query', '@context.manager', '@flysystem_factory', '@language_manager'] + islandora.gemini.client: + class: Islandora\Crayfish\Commons\Client\GeminiClient + factory: ['Drupal\islandora\GeminiClientFactory', create] + arguments: ['@config.factory', '@logger.channel.islandora'] + islandora.gemini.lookup: + class: Drupal\islandora\GeminiLookup + arguments: ['@islandora.gemini.client', '@jwt.authentication.jwt', '@islandora.media_source_service', '@http_client', '@logger.channel.islandora'] diff --git a/islandora.views.inc b/islandora.views.inc new file mode 100644 index 00000000..cd826d08 --- /dev/null +++ b/islandora.views.inc @@ -0,0 +1,25 @@ +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'; + } + } +} diff --git a/migrate/tags.csv b/migrate/tags.csv index d7766c44..60705a2b 100644 --- a/migrate/tags.csv +++ b/migrate/tags.csv @@ -11,3 +11,7 @@ islandora_models,"Binary","A generic binary file for repository items that don't islandora_models,"Collection","A collection is an aggregation of items",http://purl.org/dc/dcmitype/Collection islandora_models,"Image","A visual representation other than text, including all types of moving image and still image",http://purl.org/coar/resource_type/c_c513 islandora_models,"Video","A recording of visual images, usually in motion and with sound accompaniment",http://purl.org/coar/resource_type/c_12ce +islandora_models,"Digital Document","An electronic file or document.",https://schema.org/DigitalDocument +islandora_models,"Paged Content","An Electronic Book, object with pages",https://schema.org/Book +islandora_models,"Page","A page in an Electronic Paged Content Object",http://id.loc.gov/ontologies/bibframe/part +islandora_models,"Publication Issue","A part of a successively published publication such as a periodical or publication volume, often numbered, usually containing a grouping of works such as articles.",https://schema.org/PublicationIssue diff --git a/modules/islandora_audio/LICENSE b/modules/islandora_audio/LICENSE new file mode 100644 index 00000000..ecbc0593 --- /dev/null +++ b/modules/islandora_audio/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/modules/islandora_audio/README.md b/modules/islandora_audio/README.md new file mode 100644 index 00000000..9d12b386 --- /dev/null +++ b/modules/islandora_audio/README.md @@ -0,0 +1,23 @@ +# Islandora Video +[![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) + +## Introduction + +Islandora Video module for Drupal 8.2.x + +## Maintainers + +Current maintainers: + +* [Danny Lamb](https://github.com/dannylamb) + +## Development + +If you would like to contribute, please get involved by attending our weekly [Tech Call](https://github.com/Islandora-CLAW/CLAW/wiki). 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 Licencse 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. + +## License + +[GPLv2](http://www.gnu.org/licenses/gpl-2.0.txt) diff --git a/modules/islandora_audio/config/schema/islandora_audio.info.yml b/modules/islandora_audio/config/schema/islandora_audio.info.yml new file mode 100644 index 00000000..47d08ee3 --- /dev/null +++ b/modules/islandora_audio/config/schema/islandora_audio.info.yml @@ -0,0 +1,28 @@ +action.configuration.generate_audio_derivative: + type: mapping + label: 'Generate a audio derivative...' + mapping: + queue: + type: text + label: 'Queue' + event: + type: text + label: 'Event Type' + source_term_uri: + type: text + label: 'Source term uri' + derivative_term_uri: + type: text + label: 'Destination term uri' + mimetype: + type: text + label: 'Audio Mimetype' + args: + type: text + label: 'FFMpeg Arguments' + scheme: + type: text + label: 'Flysystem scheme' + path: + type: text + label: 'File path with extension' diff --git a/modules/islandora_audio/islandora_audio.info.yml b/modules/islandora_audio/islandora_audio.info.yml new file mode 100644 index 00000000..237130d3 --- /dev/null +++ b/modules/islandora_audio/islandora_audio.info.yml @@ -0,0 +1,7 @@ +name: 'Islandora Audio' +description: 'Islandora audio derivative actions' +type: module +package: Islandora +core: 8.x +dependencies: + - islandora diff --git a/modules/islandora_audio/islandora_audio.module b/modules/islandora_audio/islandora_audio.module new file mode 100644 index 00000000..a5093c1b --- /dev/null +++ b/modules/islandora_audio/islandora_audio.module @@ -0,0 +1,32 @@ +' . t('About') . ''; + $output .= '

' . t('Islandora Audio adds audio derivative actions.') . '

'; + $output .= '

' . t('These can be used with microservices to automatically generate derivative audios.') . '

'; + return $output; + + default: + } +} diff --git a/modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php b/modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php new file mode 100644 index 00000000..41e33d55 --- /dev/null +++ b/modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php @@ -0,0 +1,56 @@ +getValue('mimetype')); + if ($exploded_mime[0] != 'audio') { + $form_state->setErrorByName( + 'mimetype', + t('Please enter a audio mimetype (e.g. audio/mpeg, audio/m4a, etc...)') + ); + } + } + +} diff --git a/modules/islandora_audio/tests/fixtures/test_file.txt b/modules/islandora_audio/tests/fixtures/test_file.txt new file mode 100644 index 00000000..e60b7106 --- /dev/null +++ b/modules/islandora_audio/tests/fixtures/test_file.txt @@ -0,0 +1 @@ +THIS IS A TEST diff --git a/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php b/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php new file mode 100644 index 00000000..bcd78f70 --- /dev/null +++ b/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php @@ -0,0 +1,85 @@ +drupalCreateUser([ + 'bypass node access', + 'administer contexts', + 'administer actions', + 'view media', + 'create media', + 'update media', + ]); + $this->drupalLogin($account); + + // Create an action to generate a audio derivative. + $this->drupalGet('admin/config/system/actions'); + $this->getSession()->getPage()->findById("edit-action")->selectOption("Generate a audio derivative"); + $this->getSession()->getPage()->pressButton(t('Create')); + $this->assertSession()->statusCodeEquals(200); + + $this->getSession()->getPage()->fillField('edit-label', "Generate audio test derivative"); + $this->getSession()->getPage()->fillField('edit-id', "generate_audio_test_derivative"); + $this->getSession()->getPage()->fillField('edit-queue', "generate-audio-test-derivative"); + $this->getSession()->getPage()->fillField("edit-source-term", $this->preservationMasterTerm->label()); + $this->getSession()->getPage()->fillField("edit-derivative-term", $this->serviceFileTerm->label()); + $this->getSession()->getPage()->fillField('edit-mimetype', "audio/mpeg"); + $this->getSession()->getPage()->fillField('edit-args', "-f mp3"); + $this->getSession()->getPage()->fillField('edit-scheme', "public"); + $this->getSession()->getPage()->fillField('edit-path', "derp.mov"); + $this->getSession()->getPage()->pressButton(t('Save')); + $this->assertSession()->statusCodeEquals(200); + + // Create a context and add the action as a derivative reaction. + $this->createContext('Test', 'test'); + $this->addPresetReaction('test', 'derivative', "generate_audio_test_derivative"); + $this->assertSession()->statusCodeEquals(200); + + // Create a new preservation master belonging to the node. + $values = [ + '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', + ]; + $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, t('Save')); + + $expected = [ + 'source_uri' => 'test_file.txt', + 'destination_uri' => "node/1/media/{$this->testMediaType->id()}/3", + 'file_upload_uri' => 'public://derp.mov', + 'mimetype' => 'audio/mpeg', + 'args' => '-f mp3', + 'queue' => 'islandora-connector-homarus', + ]; + + // Check the message gets published and is of the right shape. + $this->checkMessage($expected); + } + +} diff --git a/modules/islandora_breadcrumbs/config/install/islandora.breadcrumbs.yml b/modules/islandora_breadcrumbs/config/install/islandora.breadcrumbs.yml new file mode 100644 index 00000000..38f8be38 --- /dev/null +++ b/modules/islandora_breadcrumbs/config/install/islandora.breadcrumbs.yml @@ -0,0 +1,3 @@ +maxDepth: -1 +includeSelf: FALSE +referenceField: field_member_of diff --git a/modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml b/modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml new file mode 100644 index 00000000..7a26de0c --- /dev/null +++ b/modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml @@ -0,0 +1,12 @@ +islandora.breadcrumbs: + type: config_object + mapping: + maxDepth: + type: integer + label: 'Max Depth' + includeSelf: + type: boolean + label: 'Include Self' + referenceField: + type: string + label: 'Reference Field' diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml new file mode 100644 index 00000000..085b38bf --- /dev/null +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml @@ -0,0 +1,7 @@ +name: 'Islandora Breadcrumbs' +type: module +description: 'Builds breadcrumbs based on field_member_of relationships.' +core: 8.x +package: Islandora +dependencies: + - islandora diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml new file mode 100644 index 00000000..58e3c959 --- /dev/null +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml @@ -0,0 +1,6 @@ +services: + islandora_breadcrumbs.breadcrumb: + class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder + arguments: ['@entity_type.manager', '@config.factory'] + tags: + - { name: breadcrumb_builder, priority: 100 } diff --git a/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php new file mode 100644 index 00000000..54fe9534 --- /dev/null +++ b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php @@ -0,0 +1,111 @@ +nodeStorage = $entity_manager->getStorage('node'); + $this->config = $config_factory->get('islandora.breadcrumbs'); + } + + /** + * {@inheritdoc} + */ + public function applies(RouteMatchInterface $attributes) { + // Using getRawParameters for consistency (always gives a + // node ID string) because getParameters sometimes returns + // a node ID string and sometimes returns a node object. + $nid = $attributes->getRawParameters()->get('node'); + if (!empty($nid)) { + $node = $this->nodeStorage->load($nid); + return (!empty($node) && $node->hasField($this->config->get('referenceField')) && !$node->get($this->config->get('referenceField'))->isEmpty()); + } + } + + /** + * {@inheritdoc} + */ + public function build(RouteMatchInterface $route_match) { + + $nid = $route_match->getRawParameters()->get('node'); + $node = $this->nodeStorage->load($nid); + $breadcrumb = new Breadcrumb(); + + $chain = []; + $this->walkMembership($node, $chain); + + if (!$this->config->get('includeSelf')) { + array_pop($chain); + } + $breadcrumb->addCacheableDependency($node); + + // Add membership chain to the breadcrumb. + foreach ($chain as $chainlink) { + $breadcrumb->addCacheableDependency($chainlink); + $breadcrumb->addLink($chainlink->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()) { + $this->walkMembership($entity->get($this->config->get('referenceField'))->entity, $crumbs); + } + } + +} diff --git a/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php b/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php new file mode 100644 index 00000000..c95d362c --- /dev/null +++ b/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php @@ -0,0 +1,109 @@ +nodeA = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => $this->testType->id(), + 'title' => 'Node A', + ]); + $this->nodeA->save(); + + $this->nodeB = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => $this->testType->id(), + 'title' => 'Node B', + ]); + $this->nodeB->set('field_member_of', [$this->nodeA->id()]); + $this->nodeB->save(); + + $this->nodeC = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => $this->testType->id(), + 'title' => 'Node C', + ]); + $this->nodeC->set('field_member_of', [$this->nodeB->id()]); + $this->nodeC->save(); + + $this->nodeD = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => $this->testType->id(), + 'title' => 'Node D', + ]); + $this->nodeD->set('field_member_of', [$this->nodeC->id()]); + $this->nodeD->save(); + } + + /** + * @covers \Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder::applies + */ + public function testDefaults() { + $breadcrumbs = [ + $this->nodeC->toUrl()->toString() => $this->nodeC->label(), + $this->nodeB->toUrl()->toString() => $this->nodeB->label(), + $this->nodeA->toUrl()->toString() => $this->nodeA->label(), + ]; + $this->assertBreadcrumb($this->nodeD->toUrl()->toString(), $breadcrumbs); + + // Create a reference loop. + $this->nodeA->set('field_member_of', [$this->nodeD->id()]); + $this->nodeA->save(); + + // We should still escape it and have the same trail as before. + $this->assertBreadcrumb($this->nodeD->toUrl()->toString(), $breadcrumbs); + } + +} diff --git a/modules/islandora_core_feature/config/install/core.entity_view_display.media.file.source.yml b/modules/islandora_core_feature/config/install/core.entity_view_display.media.file.source.yml index e6718127..986ea1fa 100644 --- a/modules/islandora_core_feature/config/install/core.entity_view_display.media.file.source.yml +++ b/modules/islandora_core_feature/config/install/core.entity_view_display.media.file.source.yml @@ -30,6 +30,7 @@ content: hidden: created: true field_file_size: true + field_gemini_uri: true field_media_of: true field_media_use: true field_mime_type: true diff --git a/modules/islandora_core_feature/config/install/core.entity_view_display.media.image.source.yml b/modules/islandora_core_feature/config/install/core.entity_view_display.media.image.source.yml index e205f5a5..107ff4ff 100644 --- a/modules/islandora_core_feature/config/install/core.entity_view_display.media.image.source.yml +++ b/modules/islandora_core_feature/config/install/core.entity_view_display.media.image.source.yml @@ -15,7 +15,7 @@ dependencies: module: - islandora_core_feature module: - - image + - islandora id: media.image.source targetEntityType: media bundle: image @@ -27,12 +27,13 @@ content: image_style: '' image_link: content third_party_settings: { } - type: image + type: islandora_image weight: 0 region: content hidden: created: true field_file_size: true + field_gemini_uri: true field_height: true field_media_of: true field_media_use: true diff --git a/modules/islandora_core_feature/config/install/field.field.media.audio.field_media_use.yml b/modules/islandora_core_feature/config/install/field.field.media.audio.field_media_use.yml index 88f5be9c..2f1bdda8 100644 --- a/modules/islandora_core_feature/config/install/field.field.media.audio.field_media_use.yml +++ b/modules/islandora_core_feature/config/install/field.field.media.audio.field_media_use.yml @@ -10,7 +10,7 @@ field_name: field_media_use entity_type: media bundle: audio label: 'Media Use' -description: '' +description: 'Defined by Portland Common Data Model: Use Extension https://pcdm.org/2015/05/12/use. ''Original File'' will trigger creation of derivatives.' required: false translatable: false default_value: { } diff --git a/modules/islandora_core_feature/config/install/field.field.media.file.field_media_use.yml b/modules/islandora_core_feature/config/install/field.field.media.file.field_media_use.yml index 85eab457..ae1b6c31 100644 --- a/modules/islandora_core_feature/config/install/field.field.media.file.field_media_use.yml +++ b/modules/islandora_core_feature/config/install/field.field.media.file.field_media_use.yml @@ -10,7 +10,7 @@ field_name: field_media_use entity_type: media bundle: file label: 'Media Use' -description: '' +description: 'Defined by Portland Common Data Model: Use Extension https://pcdm.org/2015/05/12/use. ''Original File'' will trigger creation of derivatives.' required: false translatable: true default_value: { } diff --git a/modules/islandora_core_feature/config/install/field.field.media.image.field_media_use.yml b/modules/islandora_core_feature/config/install/field.field.media.image.field_media_use.yml index 5b93f475..f9f2d259 100644 --- a/modules/islandora_core_feature/config/install/field.field.media.image.field_media_use.yml +++ b/modules/islandora_core_feature/config/install/field.field.media.image.field_media_use.yml @@ -10,7 +10,7 @@ field_name: field_media_use entity_type: media bundle: image label: 'Media Use' -description: '' +description: 'Defined by Portland Common Data Model: Use Extension https://pcdm.org/2015/05/12/use. ''Original File'' will trigger creation of derivatives.' required: false translatable: true default_value: { } diff --git a/modules/islandora_core_feature/config/install/field.field.media.video.field_media_use.yml b/modules/islandora_core_feature/config/install/field.field.media.video.field_media_use.yml index e8cc3098..1fea8534 100644 --- a/modules/islandora_core_feature/config/install/field.field.media.video.field_media_use.yml +++ b/modules/islandora_core_feature/config/install/field.field.media.video.field_media_use.yml @@ -10,7 +10,7 @@ field_name: field_media_use entity_type: media bundle: video label: 'Media Use' -description: '' +description: 'Defined by Portland Common Data Model: Use Extension https://pcdm.org/2015/05/12/use. ''Original File'' will trigger creation of derivatives.' required: false translatable: true default_value: { } diff --git a/modules/islandora_core_feature/config/install/field.storage.media.field_media_use.yml b/modules/islandora_core_feature/config/install/field.storage.media.field_media_use.yml index 90a0cb1b..30b9d730 100644 --- a/modules/islandora_core_feature/config/install/field.storage.media.field_media_use.yml +++ b/modules/islandora_core_feature/config/install/field.storage.media.field_media_use.yml @@ -12,7 +12,7 @@ settings: target_type: taxonomy_term module: core locked: false -cardinality: 1 +cardinality: -1 translatable: true indexes: { } persist_with_no_fields: false diff --git a/modules/islandora_core_feature/config/install/field.storage.node.field_display_hints.yml b/modules/islandora_core_feature/config/install/field.storage.node.field_display_hints.yml index 7bbfacad..08590174 100644 --- a/modules/islandora_core_feature/config/install/field.storage.node.field_display_hints.yml +++ b/modules/islandora_core_feature/config/install/field.storage.node.field_display_hints.yml @@ -12,7 +12,7 @@ settings: target_type: taxonomy_term module: core locked: false -cardinality: -1 +cardinality: 1 translatable: true indexes: { } persist_with_no_fields: false diff --git a/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml b/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml new file mode 100644 index 00000000..97619cd2 --- /dev/null +++ b/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.field_weight +field_name: field_weight +entity_type: node +type: integer +settings: + unsigned: false + size: normal +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/islandora_core_feature/config/install/rdf.mapping.media.audio.yml b/modules/islandora_core_feature/config/install/rdf.mapping.media.audio.yml index 6fb8df40..bd2f291c 100644 --- a/modules/islandora_core_feature/config/install/rdf.mapping.media.audio.yml +++ b/modules/islandora_core_feature/config/install/rdf.mapping.media.audio.yml @@ -43,3 +43,6 @@ fieldMappings: properties: - 'schema:additionalType' mapping_type: rel + field_file_size: + properties: + - 'premis:hasSize' diff --git a/modules/islandora_core_feature/config/install/rdf.mapping.media.file.yml b/modules/islandora_core_feature/config/install/rdf.mapping.media.file.yml index e298c2db..84d72d80 100644 --- a/modules/islandora_core_feature/config/install/rdf.mapping.media.file.yml +++ b/modules/islandora_core_feature/config/install/rdf.mapping.media.file.yml @@ -43,3 +43,6 @@ fieldMappings: properties: - 'schema:additionalType' mapping_type: rel + field_file_size: + properties: + - 'premis:hasSize' diff --git a/modules/islandora_core_feature/config/install/rdf.mapping.media.image.yml b/modules/islandora_core_feature/config/install/rdf.mapping.media.image.yml index 3cc7a2ee..a4f71607 100644 --- a/modules/islandora_core_feature/config/install/rdf.mapping.media.image.yml +++ b/modules/islandora_core_feature/config/install/rdf.mapping.media.image.yml @@ -49,3 +49,6 @@ fieldMappings: field_height: properties: - 'ebucore:height' + field_file_size: + properties: + - 'premis:hasSize' diff --git a/modules/islandora_core_feature/config/install/rdf.mapping.media.video.yml b/modules/islandora_core_feature/config/install/rdf.mapping.media.video.yml index c70bc50d..2cb8c47c 100644 --- a/modules/islandora_core_feature/config/install/rdf.mapping.media.video.yml +++ b/modules/islandora_core_feature/config/install/rdf.mapping.media.video.yml @@ -43,3 +43,6 @@ fieldMappings: properties: - 'schema:additionalType' mapping_type: rel + field_file_size: + properties: + - 'premis:hasSize' diff --git a/modules/islandora_core_feature/config/install/rdf.mapping.taxonomy_term.islandora_display.yml b/modules/islandora_core_feature/config/install/rdf.mapping.taxonomy_term.islandora_display.yml new file mode 100644 index 00000000..f746b1ea --- /dev/null +++ b/modules/islandora_core_feature/config/install/rdf.mapping.taxonomy_term.islandora_display.yml @@ -0,0 +1,30 @@ +langcode: en +status: true +dependencies: + config: + - taxonomy.vocabulary.islandora_display + enforced: + module: + - islandora_core_feature + module: + - taxonomy +id: taxonomy_term.islandora_display +targetEntityType: taxonomy_term +bundle: islandora_display +types: + - 'schema:Thing' +fieldMappings: + name: + properties: + - 'dc:title' + description: + properties: + - 'dc:description' + field_external_uri: + properties: + - 'owl:sameAs' + changed: + properties: + - 'schema:dateModified' + datatype_callback: + callable: 'Drupal\rdf\CommonDataConverter::dateIso8601Value' diff --git a/modules/islandora_core_feature/config/install/system.action.delete_file_as_fedora_external_content.yml b/modules/islandora_core_feature/config/install/system.action.delete_file_as_fedora_external_content.yml new file mode 100644 index 00000000..f3b18a0c --- /dev/null +++ b/modules/islandora_core_feature/config/install/system.action.delete_file_as_fedora_external_content.yml @@ -0,0 +1,12 @@ +langcode: en +status: true +dependencies: + module: + - islandora +id: delete_file_as_fedora_external_content +label: 'Delete File as Fedora External Content' +type: file +plugin: emit_file_event +configuration: + queue: islandora-indexing-fcrepo-delete + event: Delete diff --git a/modules/islandora_core_feature/config/install/system.action.delete_media_from_triplestore.yml b/modules/islandora_core_feature/config/install/system.action.delete_media_from_triplestore.yml index 20e1d7d5..5aea5c61 100644 --- a/modules/islandora_core_feature/config/install/system.action.delete_media_from_triplestore.yml +++ b/modules/islandora_core_feature/config/install/system.action.delete_media_from_triplestore.yml @@ -12,4 +12,4 @@ type: media plugin: emit_media_event configuration: queue: islandora-indexing-triplestore-delete - event: delete + event: Delete diff --git a/modules/islandora_core_feature/config/install/system.action.delete_node_from_fedora.yml b/modules/islandora_core_feature/config/install/system.action.delete_node_from_fedora.yml index 0c91ce4d..f5e9535c 100644 --- a/modules/islandora_core_feature/config/install/system.action.delete_node_from_fedora.yml +++ b/modules/islandora_core_feature/config/install/system.action.delete_node_from_fedora.yml @@ -12,4 +12,4 @@ type: node plugin: emit_node_event configuration: queue: islandora-indexing-fcrepo-delete - event: delete + event: Delete diff --git a/modules/islandora_core_feature/config/install/system.action.delete_node_from_triplestore.yml b/modules/islandora_core_feature/config/install/system.action.delete_node_from_triplestore.yml index 8b16cf3b..0d831eb9 100644 --- a/modules/islandora_core_feature/config/install/system.action.delete_node_from_triplestore.yml +++ b/modules/islandora_core_feature/config/install/system.action.delete_node_from_triplestore.yml @@ -12,4 +12,4 @@ type: node plugin: emit_node_event configuration: queue: islandora-indexing-triplestore-delete - event: delete + event: Delete diff --git a/modules/islandora_core_feature/config/install/system.action.index_file_as_fedora_external_content.yml b/modules/islandora_core_feature/config/install/system.action.index_file_as_fedora_external_content.yml new file mode 100644 index 00000000..4b3f6ce8 --- /dev/null +++ b/modules/islandora_core_feature/config/install/system.action.index_file_as_fedora_external_content.yml @@ -0,0 +1,12 @@ +langcode: en +status: true +dependencies: + module: + - islandora +id: index_file_as_fedora_external_content +label: 'Index File as Fedora External Content' +type: file +plugin: emit_file_event +configuration: + queue: islandora-indexing-fcrepo-file-external + event: Update diff --git a/modules/islandora_core_feature/config/install/system.action.index_file_in_fedora.yml b/modules/islandora_core_feature/config/install/system.action.index_file_in_fedora.yml index 83a7ac33..17776733 100644 --- a/modules/islandora_core_feature/config/install/system.action.index_file_in_fedora.yml +++ b/modules/islandora_core_feature/config/install/system.action.index_file_in_fedora.yml @@ -12,4 +12,4 @@ type: file plugin: emit_file_event configuration: queue: islandora-indexing-fcrepo-file - event: Create + event: Update diff --git a/modules/islandora_core_feature/config/install/system.action.index_media_in_fedora.yml b/modules/islandora_core_feature/config/install/system.action.index_media_in_fedora.yml index ff26adbd..baee2bb0 100644 --- a/modules/islandora_core_feature/config/install/system.action.index_media_in_fedora.yml +++ b/modules/islandora_core_feature/config/install/system.action.index_media_in_fedora.yml @@ -12,4 +12,4 @@ type: media plugin: emit_media_event configuration: queue: islandora-indexing-fcrepo-media - event: update + event: Update diff --git a/modules/islandora_core_feature/config/install/system.action.index_media_in_triplestore.yml b/modules/islandora_core_feature/config/install/system.action.index_media_in_triplestore.yml index 6146a892..816e2fa6 100644 --- a/modules/islandora_core_feature/config/install/system.action.index_media_in_triplestore.yml +++ b/modules/islandora_core_feature/config/install/system.action.index_media_in_triplestore.yml @@ -12,4 +12,4 @@ type: media plugin: emit_media_event configuration: queue: islandora-indexing-triplestore-index - event: update + event: Update diff --git a/modules/islandora_core_feature/config/install/system.action.index_node_in_fedora.yml b/modules/islandora_core_feature/config/install/system.action.index_node_in_fedora.yml index 9b359035..085b6e5f 100644 --- a/modules/islandora_core_feature/config/install/system.action.index_node_in_fedora.yml +++ b/modules/islandora_core_feature/config/install/system.action.index_node_in_fedora.yml @@ -12,4 +12,4 @@ type: node plugin: emit_node_event configuration: queue: islandora-indexing-fcrepo-content - event: update + event: Update diff --git a/modules/islandora_core_feature/config/install/system.action.index_node_in_triplestore.yml b/modules/islandora_core_feature/config/install/system.action.index_node_in_triplestore.yml index ba194f51..3aa8842e 100644 --- a/modules/islandora_core_feature/config/install/system.action.index_node_in_triplestore.yml +++ b/modules/islandora_core_feature/config/install/system.action.index_node_in_triplestore.yml @@ -12,4 +12,4 @@ type: node plugin: emit_node_event configuration: queue: islandora-indexing-triplestore-index - event: update + event: Update diff --git a/modules/islandora_core_feature/config/install/views.view.display_media.yml b/modules/islandora_core_feature/config/install/views.view.display_media.yml index d73b039e..397bb8f4 100644 --- a/modules/islandora_core_feature/config/install/views.view.display_media.yml +++ b/modules/islandora_core_feature/config/install/views.view.display_media.yml @@ -264,6 +264,84 @@ display: - url - user.permissions tags: { } + entity_view_2: + display_plugin: entity_view + id: entity_view_2 + display_title: 'Original File - Download' + position: 1 + display_options: + display_extenders: { } + display_description: 'The original creation format of a file' + filters: + status: + value: '1' + table: media_field_data + field: status + plugin_id: boolean + entity_type: media + entity_field: status + id: status + expose: + operator: '' + group: 1 + field_external_uri_uri: + id: field_external_uri_uri + table: taxonomy_term__field_external_uri + field: field_external_uri_uri + relationship: field_media_use + group_type: group + admin_label: '' + operator: '=' + value: 'http://pcdm.org/use#OriginalFile' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + placeholder: '' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + defaults: + filters: false + filter_groups: false + filter_groups: + operator: AND + groups: + 1: AND + entity_type: node + bundles: { } + argument_mode: id + default_argument: null + title: 'Original File' + show_title: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user.permissions + tags: { } service_file: display_plugin: entity_view id: service_file diff --git a/modules/islandora_core_feature/config/install/views.view.manage_members.yml b/modules/islandora_core_feature/config/install/views.view.manage_members.yml index 68208b04..a978f1d2 100644 --- a/modules/islandora_core_feature/config/install/views.view.manage_members.yml +++ b/modules/islandora_core_feature/config/install/views.view.manage_members.yml @@ -294,7 +294,7 @@ display: path: node/%node/members menu: type: tab - title: Members + title: Children description: '' expanded: false parent: '' diff --git a/modules/islandora_core_feature/config/install/views.view.members.yml b/modules/islandora_core_feature/config/install/views.view.members.yml index ae7a0d0a..bb468416 100644 --- a/modules/islandora_core_feature/config/install/views.view.members.yml +++ b/modules/islandora_core_feature/config/install/views.view.members.yml @@ -225,7 +225,7 @@ display: filter_groups: operator: AND groups: { } - title: Members + title: Children cache_metadata: max-age: -1 contexts: diff --git a/modules/islandora_core_feature/config/install/views.view.reorder_children.yml b/modules/islandora_core_feature/config/install/views.view.reorder_children.yml new file mode 100644 index 00000000..b262141d --- /dev/null +++ b/modules/islandora_core_feature/config/install/views.view.reorder_children.yml @@ -0,0 +1,271 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - islandora_core_feature + module: + - islandora + - node + - user +id: reorder_children +label: 'Reorder children' +module: views +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 + 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: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + tags: + previous: ‹‹ + next: ›› + first: '« First' + last: 'Last »' + expose: + items_per_page: true + items_per_page_label: 'Items per page' + items_per_page_options: '10, 25, 50, 100' + items_per_page_options_all: true + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + quantity: 9 + style: + type: table + row: + type: fields + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: 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: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + 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 + field_weight: + id: field_weight + table: node__field_weight + field: field_weight + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + 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: false + 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: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + plugin_id: integer_weight_selector + filters: { } + sorts: + field_weight_value: + id: field_weight_value + table: node__field_weight + field: field_weight_value + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + plugin_id: standard + title: 'Reorder children' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + field_member_of_target_id: + id: field_member_of_target_id + table: node__field_member_of + field: field_member_of_target_id + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: node + default_argument_options: { } + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: true + validate: + type: 'entity:node' + fail: 'not found' + validate_options: + operation: view + multiple: 0 + bundles: { } + access: false + break_phrase: false + not: false + plugin_id: numeric + display_extenders: { } + filter_groups: + operator: AND + groups: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: node/%node/reorder + menu: + type: tab + title: 'Reorder Children' + description: '' + expanded: false + parent: '' + weight: 0 + context: '0' + menu_name: main + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/modules/islandora_core_feature/islandora_core_feature.features.yml b/modules/islandora_core_feature/islandora_core_feature.features.yml old mode 100644 new mode 100755 index 425bfa80..c3ff6620 --- a/modules/islandora_core_feature/islandora_core_feature.features.yml +++ b/modules/islandora_core_feature/islandora_core_feature.features.yml @@ -10,7 +10,5 @@ excluded: - core.entity_form_display.taxonomy_term.islandora_display.default - core.entity_form_display.taxonomy_term.islandora_media_use.default - core.entity_form_display.taxonomy_term.islandora_models.default - - language.content_settings.taxonomy_term.islandora_access - - field.storage.node.field_access_terms - taxonomy.vocabulary.islandora_access required: true diff --git a/modules/islandora_core_feature/islandora_core_feature.info.yml b/modules/islandora_core_feature/islandora_core_feature.info.yml old mode 100644 new mode 100755 diff --git a/modules/islandora_iiif/config/install/islandora_iiif.settings.yml b/modules/islandora_iiif/config/install/islandora_iiif.settings.yml new file mode 100644 index 00000000..760946fb --- /dev/null +++ b/modules/islandora_iiif/config/install/islandora_iiif.settings.yml @@ -0,0 +1 @@ +iiif_server: diff --git a/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml new file mode 100644 index 00000000..6ef42bc4 --- /dev/null +++ b/modules/islandora_iiif/config/schema/islandora_iiif.schema.yml @@ -0,0 +1,7 @@ +islandora_iiif.settings: + type: config_object + label: 'Islandora IIIF Settings' + mapping: + iiif_server: + type: string + label: 'IIIF Server Url' diff --git a/modules/islandora_iiif/islandora_iiif.info.yml b/modules/islandora_iiif/islandora_iiif.info.yml new file mode 100644 index 00000000..06c249e1 --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.info.yml @@ -0,0 +1,7 @@ +name: 'Islandora IIIF' +type: module +description: 'IIIF support for Islandora' +core: 8.x +package: Islandora +dependencies: + - islandora diff --git a/modules/islandora_iiif/islandora_iiif.links.menu.yml b/modules/islandora_iiif/islandora_iiif.links.menu.yml new file mode 100644 index 00000000..4d5b5060 --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.links.menu.yml @@ -0,0 +1,6 @@ +islandora_iiif.islandora_iiif_config_form: + title: 'IIIF Settings' + route_name: islandora_iiif.islandora_iiif_config_form + description: 'Configure Islandora IIIF settings' + parent: system.admin_config_islandora + weight: 99 diff --git a/modules/islandora_iiif/islandora_iiif.module b/modules/islandora_iiif/islandora_iiif.module new file mode 100644 index 00000000..e9e7526e --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.module @@ -0,0 +1,24 @@ +' . t('About') . ''; + $output .= '

' . t('IIIF support for Islandora') . '

'; + return $output; + + default: + } +} diff --git a/modules/islandora_iiif/islandora_iiif.routing.yml b/modules/islandora_iiif/islandora_iiif.routing.yml new file mode 100644 index 00000000..dd69c2e5 --- /dev/null +++ b/modules/islandora_iiif/islandora_iiif.routing.yml @@ -0,0 +1,9 @@ +islandora_iiif.islandora_iiif_config_form: + path: '/admin/config/islandora/iiif' + defaults: + _form: '\Drupal\islandora_iiif\Form\IslandoraIIIFConfigForm' + _title: 'IslandoraIIIFConfigForm' + requirements: + _permission: 'access administration pages' + options: + _admin_route: TRUE diff --git a/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php b/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php new file mode 100644 index 00000000..5bb73de4 --- /dev/null +++ b/modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php @@ -0,0 +1,92 @@ +config('islandora_iiif.settings'); + $form['iiif_server'] = [ + '#type' => 'url', + '#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'), + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if (!empty($form_state->getValue('iiif_server'))) { + $server = $form_state->getValue('iiif_server'); + if (!UrlHelper::isValid($server, UrlHelper::isExternal($server))) { + $form_state->setErrorByName('iiif_server', "IIIF Server address is not a valid URL"); + } + elseif (!$this->validateIiifUrl($server)) { + $form_state->setErrorByName('iiif_server', "IIIF Server does not seem to be accessible."); + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + $this->config('islandora_iiif.settings') + ->set('iiif_server', $form_state->getValue('iiif_server')) + ->save(); + } + + /** + * Ensure the IIIF server is accessible. + * + * @param string $server_uri + * The absolute or relative URI to the server. + * + * @return bool + * True if server returns 200 on a HEAD request. + */ + private function validateIiifUrl($server_uri) { + $client = \Drupal::httpClient(); + try { + $result = $client->head($server_uri); + return ($result->getStatusCode() == 200); + } + catch (ClientException $e) { + return FALSE; + } + + } + +} diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php new file mode 100644 index 00000000..0e33199b --- /dev/null +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -0,0 +1,329 @@ +serializer = $serializer; + $this->request = $request; + $this->iiifConfig = $iiif_config; + $this->fileSystem = $file_system; + $this->httpClient = $http_client; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('serializer'), + $container->get('request_stack')->getCurrentRequest(), + $container->get('config.factory')->get('islandora_iiif.settings'), + $container->get('file_system'), + $container->get('http_client') + ); + } + + /** + * {@inheritdoc} + */ + public function render() { + $json = []; + $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(); + // 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); + array_pop($url_components); + $iiif_base_id = implode('/', $url_components); + // @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', + '@context' => 'http://iiif.io/api/presentation/2/context.json', + // @see https://iiif.io/api/presentation/2.1/#sequence + 'sequences' => [ + [ + '@context' => 'http://iiif.io/api/presentation/2/context.json', + '@id' => $iiif_base_id . '/sequence/normal', + '@type' => 'sc:Sequence', + ], + ], + ]; + // For each row in the View result. + foreach ($this->view->result as $row) { + // Add the IIIF URL to the image to print out as JSON. + $canvases = $this->getTileSourceFromRow($row, $iiif_address, $iiif_base_id); + foreach ($canvases as $tile_source) { + $json['sequences'][0]['canvases'][] = $tile_source; + } + } + } + unset($this->view->row_index); + + $content_type = 'json'; + + return $this->serializer->serialize($json, $content_type, ['views_style_plugin' => $this]); + } + + /** + * Render array from views result row. + * + * @param \Drupal\views\ResultRow $row + * Result row. + * @param string $iiif_address + * The URL to the IIIF server endpoint. + * @param string $iiif_base_id + * The URL for the request, minus the last part of the URL, + * which is likely "manifest". + * + * @return array + * List of IIIF URLs to display in the Openseadragon viewer. + */ + protected function getTileSourceFromRow(ResultRow $row, $iiif_address, $iiif_base_id) { + $canvases = []; + foreach ($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) { + // Create the IIIF URL for this file + // Visiting $iiif_url will resolve to the info.json for the image. + $file_url = $image->entity->url(); + $mime_type = $image->entity->getMimeType(); + $iiif_url = rtrim($iiif_address, '/') . '/' . urlencode($file_url); + + // Create the necessary ID's for the canvas and annotation. + $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 $e) { + } + catch (ServerException $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]; + } + } + } + + $canvases[] = [ + // @see https://iiif.io/api/presentation/2.1/#canvas + '@id' => $canvas_id, + '@type' => 'sc:Canvas', + 'label' => $image->entity->label(), + 'height' => $height, + 'width' => $width, + // @see https://iiif.io/api/presentation/2.1/#image-resources + 'images' => [ + [ + '@id' => $annotation_id, + "@type" => "oa:Annotation", + 'motivation' => 'sc:painting', + 'resource' => [ + '@id' => $iiif_url . '/full/full/0/default.jpg', + "@type" => "dctypes:Image", + 'format' => $mime_type, + 'height' => $height, + 'width' => $width, + 'service' => [ + '@id' => $iiif_url, + '@context' => 'http://iiif.io/api/image/2/context.json', + 'profile' => 'http://iiif.io/api/image/2/profiles/level2.json', + ], + ], + 'on' => $canvas_id, + ], + ], + ]; + } + } + } + + return $canvases; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['iiif_tile_field'] = ['default' => '']; + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $field_options = []; + + $fields = $this->displayHandler->getHandlers('field'); + $islandora_default_file_fields = [ + 'field_media_file', + 'field_media_image', + ]; + $file_views_field_formatters = [ + // Image formatters. + 'image', 'image_url', + // File formatters. + 'file_default', 'file_url_plain', + ]; + /** @var \Drupal\views\Plugin\views\field\FieldPluginBase[] $fields */ + foreach ($fields as $field_name => $field) { + // If this is a known Islandora file/image field + // OR it is another/custom field add it as an available option. + // @todo find better way to identify file fields + // Currently $field->options['type'] is storing the "formatter" of the + // file/image so there are a lot of possibilities. + // The default formatters are 'image' and 'file_default' + // so this approach should catch most... + if (in_array($field_name, $islandora_default_file_fields) || + (!empty($field->options['type']) && in_array($field->options['type'], $file_views_field_formatters))) { + $field_options[$field_name] = $field->adminLabel(); + } + } + + // If no fields to choose from, add an error message indicating such. + if (count($field_options) == 0) { + drupal_set_message($this->t('No image or file fields were found in the View. + You will need to add a field to this View'), 'error'); + } + + $form['iiif_tile_field'] = [ + '#title' => $this->t('Tile source field(s)'), + '#type' => 'checkboxes', + '#default_value' => $this->options['iiif_tile_field'], + '#description' => $this->t("The source of image for each entity."), + '#options' => $field_options, + // Only make the form element required if + // we have more than one option to choose from + // otherwise could lock up the form when setting up a View. + '#required' => count($field_options) > 0, + ]; + } + + /** + * Returns an array of format options. + * + * @return string[] + * An array of the allowed serializer formats. In this case just JSON. + */ + public function getFormats() { + return ['json' => 'json']; + } + +} diff --git a/modules/islandora_image/.travis.yml b/modules/islandora_image/.travis.yml new file mode 100644 index 00000000..41cf7d9e --- /dev/null +++ b/modules/islandora_image/.travis.yml @@ -0,0 +1,41 @@ + +sudo: true +language: php +php: + - 7.1 + - 7.2 + +matrix: + fast_finish: true + +branches: + only: + - /^8.x/ + +before_install: + - export SCRIPT_DIR=$HOME/CLAW/.scripts + - export DRUPAL_DIR=/opt/drupal + - export COMPOSER_PATH="/home/travis/.phpenv/versions/$TRAVIS_PHP_VERSION/bin/composer" + +install: + - git clone https://github.com/Islandora-CLAW/CLAW.git $HOME/CLAW + - $SCRIPT_DIR/travis_setup_drupal.sh + - git -C "$TRAVIS_BUILD_DIR" checkout -b travis-testing + - cd $DRUPAL_DIR; + - php -dmemory_limit=-1 $COMPOSER_PATH config repositories.local path "$TRAVIS_BUILD_DIR" + - php -dmemory_limit=-1 $COMPOSER_PATH require "islandora/islandora_image:dev-travis-testing as dev-8.x-1.x" --prefer-source + - cd web; drush en -y islandora_image + +script: + - $SCRIPT_DIR/line_endings.sh $TRAVIS_BUILD_DIR + - phpcs --standard=Drupal --ignore=*.md --extensions=php,module,inc,install,test,profile,theme,css,info $TRAVIS_BUILD_DIR + - phpcpd --names *.module,*.inc,*.test,*.php $TRAVIS_BUILD_DIR + - php core/scripts/run-tests.sh --url http://127.0.0.1:8282 --suppress-deprecations --verbose --php `which php` --module "islandora_image" + +notifications: + irc: + channels: + - "irc.freenode.org#islandora" + on_success: change + on_failure: always + skip_join: true diff --git a/modules/islandora_image/LICENSE b/modules/islandora_image/LICENSE new file mode 100644 index 00000000..ecbc0593 --- /dev/null +++ b/modules/islandora_image/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/modules/islandora_image/README.md b/modules/islandora_image/README.md new file mode 100644 index 00000000..119ad359 --- /dev/null +++ b/modules/islandora_image/README.md @@ -0,0 +1,23 @@ +# ![Islandora Image](https://cloud.githubusercontent.com/assets/2371345/24199472/6f7bfb7a-0ee8-11e7-9c94-754762fd5566.png) Islandora Image +[![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) + +## Introduction + +Islandora Image module for Drupal 8.2.x + +## Maintainers + +Current maintainers: + +* [Danny Lamb](https://github.com/dannylamb) + +## Development + +If you would like to contribute, please get involved by attending our weekly [Tech Call](https://github.com/Islandora-CLAW/CLAW/wiki). 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 Licencse 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. + +## License + +[GPLv2](http://www.gnu.org/licenses/gpl-2.0.txt) diff --git a/modules/islandora_image/config/schema/islandora_image.info.yml b/modules/islandora_image/config/schema/islandora_image.info.yml new file mode 100644 index 00000000..350516d0 --- /dev/null +++ b/modules/islandora_image/config/schema/islandora_image.info.yml @@ -0,0 +1,28 @@ +action.configuration.generate_image_derivative: + type: mapping + label: 'Generate an image derivative...' + mapping: + queue: + type: text + label: 'Queue' + event: + type: text + label: 'Event Type' + source_term_uri: + type: text + label: 'Source term uri' + derivative_term_uri: + type: text + label: 'Destination term uri' + mimetype: + type: text + label: 'Image Mimetype' + args: + type: text + label: 'Convert Arguments' + scheme: + type: text + label: 'Flysystem scheme' + path: + type: text + label: 'File path with extension' diff --git a/modules/islandora_image/islandora_image.info.yml b/modules/islandora_image/islandora_image.info.yml new file mode 100644 index 00000000..fe5ef2cf --- /dev/null +++ b/modules/islandora_image/islandora_image.info.yml @@ -0,0 +1,7 @@ +name: 'Islandora Image' +type: module +description: 'Islandora Image derivative actions' +core: 8.x +package: Islandora +dependencies: + - islandora diff --git a/modules/islandora_image/islandora_image.module b/modules/islandora_image/islandora_image.module new file mode 100644 index 00000000..41a7a585 --- /dev/null +++ b/modules/islandora_image/islandora_image.module @@ -0,0 +1,24 @@ +' . t('About') . ''; + $output .= '

' . t('Islandora Image') . '

'; + return $output; + + default: + } +} diff --git a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php new file mode 100644 index 00000000..fa713a71 --- /dev/null +++ b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php @@ -0,0 +1,56 @@ +getValue('mimetype')); + + if ($exploded_mime[0] != "image") { + $form_state->setErrorByName( + 'mimetype', + t('Please enter an image mimetype (e.g. image/jpeg, image/png, etc...)') + ); + } + } + +} diff --git a/modules/islandora_image/tests/fixtures/test_file.txt b/modules/islandora_image/tests/fixtures/test_file.txt new file mode 100644 index 00000000..fafd7451 --- /dev/null +++ b/modules/islandora_image/tests/fixtures/test_file.txt @@ -0,0 +1 @@ +TEST FILE diff --git a/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php b/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php new file mode 100644 index 00000000..c917ba6c --- /dev/null +++ b/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php @@ -0,0 +1,87 @@ +drupalCreateUser([ + 'bypass node access', + 'administer contexts', + 'administer actions', + 'view media', + 'create media', + 'update media', + ]); + $this->drupalLogin($account); + + // Create an action to generate a jpeg thumbnail. + $this->drupalGet('admin/config/system/actions'); + $this->getSession()->getPage()->findById("edit-action")->selectOption("Generate an image derivative"); + $this->getSession()->getPage()->pressButton(t('Create')); + $this->assertSession()->statusCodeEquals(200); + + $this->getSession()->getPage()->fillField('edit-label', "Generate image test derivative"); + $this->getSession()->getPage()->fillField('edit-id', "generate_image_test_derivative"); + $this->getSession()->getPage()->fillField('edit-queue', "generate-image-test-derivative"); + $this->getSession()->getPage()->fillField("edit-source-term", $this->preservationMasterTerm->label()); + $this->getSession()->getPage()->fillField("edit-derivative-term", $this->serviceFileTerm->label()); + $this->getSession()->getPage()->fillField('edit-mimetype', "image/jpeg"); + $this->getSession()->getPage()->fillField('edit-args', "-thumbnail 20x20"); + $this->getSession()->getPage()->fillField('edit-scheme', "public"); + $this->getSession()->getPage()->fillField('edit-path', "derp.jpeg"); + $this->getSession()->getPage()->pressButton(t('Save')); + $this->assertSession()->statusCodeEquals(200); + + // Create a context and add the action as a derivative reaction. + $this->createContext('Test', 'test'); + $this->addPresetReaction('test', 'derivative', "generate_image_test_derivative"); + $this->assertSession()->statusCodeEquals(200); + + // Create a new preservation master belonging to the node. + $values = [ + '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', + ]; + $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, t('Save')); + + $expected = [ + 'source_uri' => 'test_file.txt', + 'destination_uri' => "node/1/media/{$this->testMediaType->id()}/3", + 'file_upload_uri' => 'public://derp.jpeg', + 'mimetype' => 'image/jpeg', + 'args' => '-thumbnail 20x20', + 'queue' => 'islandora-connector-houdini', + ]; + + // Check the message gets published and is of the right shape. + $this->checkMessage($expected); + } + +} diff --git a/modules/islandora_text_extraction/LICENSE b/modules/islandora_text_extraction/LICENSE new file mode 100644 index 00000000..ecbc0593 --- /dev/null +++ b/modules/islandora_text_extraction/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/modules/islandora_text_extraction/README.md b/modules/islandora_text_extraction/README.md new file mode 100644 index 00000000..84daf0db --- /dev/null +++ b/modules/islandora_text_extraction/README.md @@ -0,0 +1,44 @@ +# islandora_text_extraction +### Connects Islandora 8 to Hypercube microservice and extracts text from PDFs + +Install module in the usual way, +then copy `assets/ca.islandora.alpaca.connector.ocr.blueprint.xml` +to `/opt/karaf/deploy` on the server. + _note:_ This config file assumes a url of `http://localhost:8000/hypercube`. +If your service is found elsewhere this must be changed. +There is no need to restart. + +In the usual Ansible build this will require no modification. + +If a parent node is tagged as `Digital Document` an `Image` tagged media +will extract text from that image at the time of ingestion. +The content type of the parent node should be configured to allow multiple tags. + +_note:_ Media are linked to their parent nodes with the `Media Of` +entity reference field. If you wish to attach the PDF (or any other ) media type +to a parent node which has any content type other than Repository Item +(islandora_object) the parent content type will have to be added to the `Media Of` +field in the media type description. + +## Prepare module for PDF text extraction +Install `texttopdf` on your server if not already present. +On an ubuntu/debian machine like the default claw playbook run +`sudo apt-get install poppler-utils` + +test to see its been properly installed with `which pdftotext` + +Install php libraries with `composer require spatie/pdf-to-text` + +In the unlikely event that your `pdftotext` binary exists on your server +outside of the system path, the path to the binary can be set at +`/admin/config/islandora/text_extraction`. + +## Using text extraction ## +The containing document must be tagged as `Digital Document`, +and the media must be tagged as `Original File`. +A new editable `Extracted Text` media will be created and attached when `PDF` or +`Image` media types are added to a node. + + + + diff --git a/modules/islandora_text_extraction/islandora_text_extraction.info.yml b/modules/islandora_text_extraction/islandora_text_extraction.info.yml new file mode 100644 index 00000000..a22048f2 --- /dev/null +++ b/modules/islandora_text_extraction/islandora_text_extraction.info.yml @@ -0,0 +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 +package: 'Islandora' +dependencies: + - islandora diff --git a/modules/islandora_text_extraction/islandora_text_extraction.install b/modules/islandora_text_extraction/islandora_text_extraction.install new file mode 100644 index 00000000..95c857f9 --- /dev/null +++ b/modules/islandora_text_extraction/islandora_text_extraction.install @@ -0,0 +1,23 @@ +getSettings(); + $extensions = $fieldSettings['file_extensions']; + if (!strpos($extensions, 'txt')) { + $fieldSettings['file_extensions'] .= ' txt'; + $field->set('settings', $fieldSettings); + $field->save(); + } +} diff --git a/modules/islandora_text_extraction/islandora_text_extraction.module b/modules/islandora_text_extraction/islandora_text_extraction.module new file mode 100644 index 00000000..2b2c1a5d --- /dev/null +++ b/modules/islandora_text_extraction/islandora_text_extraction.module @@ -0,0 +1,53 @@ +' . t('About') . ''; + $output .= '

' . t('Islandora 8 module to connect to Hypercube microservice') . '

'; + return $output; + + default: + } +} + +/** + * Implements hook_media_presave(). + */ +function islandora_text_extraction_media_presave(MediaInterface $media) { + if ($media->bundle() != 'extracted_text') { + return; + } + $text = $media->get('field_edited_text')->getValue(); + if (!$text) { + $file_id = $media->get('field_media_file')->getValue()[0]['target_id']; + $file = File::load($file_id); + $data = file_get_contents($file->getFileUri()); + $data = nl2br($data); + $media->set('field_edited_text', $data); + $media->field_edited_text->format = 'basic_html'; + } +} + +/** + * Implements hook_form_form_id_alter(). + */ +function islandora_text_extraction_form_block_form_alter(&$form, FormStateInterface $form_state, $form_id) { + unset($form['visibility']['ocr_requested']); + unset($form['visibility']['pdf_text_extraction_requested']); +} diff --git a/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php new file mode 100644 index 00000000..752d261b --- /dev/null +++ b/modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php @@ -0,0 +1,58 @@ +getValue('mimetype')); + if ($exploded_mime[0] != 'text') { + $form_state->setErrorByName( + 'mimetype', + t('Please enter file mimetype (e.g. text/plain.)') + ); + } + } + +} diff --git a/modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php b/modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php new file mode 100644 index 00000000..d21363fc --- /dev/null +++ b/modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php @@ -0,0 +1,82 @@ + $item) { + $elements[$delta] = ['#markup' => $this->viewValue($item)]; + } + + return $elements; + } + + /** + * Generate the output appropriate for one field item. + * + * @param \Drupal\Core\Field\FieldItemInterface $item + * One field item. + * + * @return string + * The textual output generated. + */ + protected function viewValue(FieldItemInterface $item) { + $fileItem = $item->getValue(); + $file = File::load($fileItem['target_id']); + $contents = file_get_contents($file->getFileUri()); + if (mb_detect_encoding($contents) != 'UTF-8') { + $contents = utf8_encode($contents); + } + $contents = nl2br($contents); + return $contents; + } + +} diff --git a/modules/islandora_text_extraction/tests/src/Functional/LoadTest.php b/modules/islandora_text_extraction/tests/src/Functional/LoadTest.php new file mode 100644 index 00000000..3cdeca91 --- /dev/null +++ b/modules/islandora_text_extraction/tests/src/Functional/LoadTest.php @@ -0,0 +1,46 @@ +user = $this->drupalCreateUser(['administer site configuration']); + $this->drupalLogin($this->user); + } + + /** + * Tests that the home page loads with a 200 response. + */ + public function testLoad() { + $this->drupalGet(Url::fromRoute('')); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/modules/islandora_text_extraction_defaults/config/install/core.entity_form_display.media.extracted_text.default.yml b/modules/islandora_text_extraction_defaults/config/install/core.entity_form_display.media.extracted_text.default.yml new file mode 100644 index 00000000..554646fd --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/core.entity_form_display.media.extracted_text.default.yml @@ -0,0 +1,81 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.extracted_text.field_edited_text + - field.field.media.extracted_text.field_media_file + - field.field.media.extracted_text.field_media_of + - field.field.media.extracted_text.field_media_use + - field.field.media.extracted_text.field_mime_type + - media.type.extracted_text + module: + - file + - path + - text +id: media.extracted_text.default +targetEntityType: media +bundle: extracted_text +mode: default +content: + created: + type: datetime_timestamp + weight: 3 + region: content + settings: { } + third_party_settings: { } + field_edited_text: + type: text_textarea + weight: 7 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } + field_media_file: + type: file_generic + weight: 6 + region: content + settings: + progress_indicator: throbber + third_party_settings: { } + langcode: + type: language_select + weight: 1 + region: content + settings: + include_locked: true + third_party_settings: { } + name: + type: string_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + path: + type: path + weight: 4 + region: content + settings: { } + third_party_settings: { } + status: + type: boolean_checkbox + settings: + display_label: true + weight: 5 + region: content + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 2 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + region: content + third_party_settings: { } +hidden: + field_media_of: true + field_media_use: true + field_mime_type: true diff --git a/modules/islandora_text_extraction_defaults/config/install/core.entity_view_display.media.extracted_text.default.yml b/modules/islandora_text_extraction_defaults/config/install/core.entity_view_display.media.extracted_text.default.yml new file mode 100644 index 00000000..7d0be6f2 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/core.entity_view_display.media.extracted_text.default.yml @@ -0,0 +1,65 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.extracted_text.field_edited_text + - field.field.media.extracted_text.field_media_file + - field.field.media.extracted_text.field_media_of + - field.field.media.extracted_text.field_media_use + - field.field.media.extracted_text.field_mime_type + - media.type.extracted_text + module: + - file + - text + - user +id: media.extracted_text.default +targetEntityType: media +bundle: extracted_text +mode: default +content: + created: + label: hidden + type: timestamp + weight: 1 + region: content + settings: + date_format: medium + custom_date_format: '' + timezone: '' + third_party_settings: { } + field_edited_text: + type: text_default + weight: 3 + region: content + label: above + settings: { } + third_party_settings: { } + field_media_file: + type: file_default + weight: 2 + region: content + label: above + settings: + use_description_as_link_text: true + third_party_settings: { } + field_media_of: + type: entity_reference_label + weight: 4 + region: content + label: above + settings: + link: true + third_party_settings: { } + uid: + label: hidden + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: + field_media_use: true + field_mime_type: true + langcode: true + name: true + thumbnail: true diff --git a/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_edited_text.yml b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_edited_text.yml new file mode 100644 index 00000000..dad71376 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_edited_text.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_edited_text + - media.type.extracted_text + module: + - text +id: media.extracted_text.field_edited_text +field_name: field_edited_text +entity_type: media +bundle: extracted_text +label: 'Edited Text' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: text_long diff --git a/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_file.yml b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_file.yml new file mode 100644 index 00000000..5e14d8af --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_file.yml @@ -0,0 +1,26 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_file + - media.type.extracted_text + module: + - file +id: media.extracted_text.field_media_file +field_name: field_media_file +entity_type: media +bundle: extracted_text +label: File +description: '' +required: true +translatable: true +default_value: { } +default_value_callback: '' +settings: + file_extensions: 'txt doc docx pdf' + file_directory: '[date:custom:Y]-[date:custom:m]' + max_filesize: '' + description_field: false + handler: 'default:file' + handler_settings: { } +field_type: file diff --git a/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_of.yml b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_of.yml new file mode 100644 index 00000000..a1f8b40b --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_of.yml @@ -0,0 +1,24 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_of + - media.type.extracted_text +id: media.extracted_text.field_media_of +field_name: field_media_of +entity_type: media +bundle: extracted_text +label: 'Media of' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + handler: 'default:node' + handler_settings: + target_bundles: null + sort: + field: _none + auto_create: false +field_type: entity_reference diff --git a/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_use.yml b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_use.yml new file mode 100644 index 00000000..6a4a1464 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_use.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_use + - media.type.extracted_text + - taxonomy.vocabulary.islandora_media_use +id: media.extracted_text.field_media_use +field_name: field_media_use +entity_type: media +bundle: extracted_text +label: 'Media Use' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + handler: 'default:taxonomy_term' + handler_settings: + target_bundles: + islandora_media_use: islandora_media_use + sort: + field: name + direction: asc + auto_create: false + auto_create_bundle: '' +field_type: entity_reference diff --git a/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_mime_type.yml b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_mime_type.yml new file mode 100644 index 00000000..5666fb2b --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_mime_type.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_mime_type + - media.type.extracted_text +id: media.extracted_text.field_mime_type +field_name: field_mime_type +entity_type: media +bundle: extracted_text +label: 'MIME type' +description: '' +required: false +translatable: true +default_value: + - + value: text/plain +default_value_callback: '' +settings: { } +field_type: string diff --git a/modules/islandora_text_extraction_defaults/config/install/field.storage.media.field_edited_text.yml b/modules/islandora_text_extraction_defaults/config/install/field.storage.media.field_edited_text.yml new file mode 100644 index 00000000..d8025161 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/field.storage.media.field_edited_text.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - media + - text +id: media.field_edited_text +field_name: field_edited_text +entity_type: media +type: text_long +settings: { } +module: text +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/islandora_text_extraction_defaults/config/install/language.content_settings.media.extracted_text.yml b/modules/islandora_text_extraction_defaults/config/install/language.content_settings.media.extracted_text.yml new file mode 100644 index 00000000..a8d2512c --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/language.content_settings.media.extracted_text.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + config: + - media.type.extracted_text +id: media.extracted_text +target_entity_type_id: media +target_bundle: extracted_text +default_langcode: site_default +language_alterable: false diff --git a/modules/islandora_text_extraction_defaults/config/install/media.type.extracted_text.yml b/modules/islandora_text_extraction_defaults/config/install/media.type.extracted_text.yml new file mode 100644 index 00000000..03c161fb --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/media.type.extracted_text.yml @@ -0,0 +1,12 @@ +langcode: en +status: true +dependencies: { } +id: extracted_text +label: 'Extracted Text' +description: 'Text extracted from Images or PDFs' +source: file +queue_thumbnail_downloads: false +new_revision: false +source_configuration: + source_field: field_media_file +field_map: { } diff --git a/modules/islandora_text_extraction_defaults/config/install/system.action.get_ocr_from_image.yml b/modules/islandora_text_extraction_defaults/config/install/system.action.get_ocr_from_image.yml new file mode 100644 index 00000000..0fc3a640 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/system.action.get_ocr_from_image.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - islandora_text_extraction +id: get_ocr_from_image +label: 'Extract Text from Image or PDF' +type: node +plugin: generate_ocr_derivative +configuration: + queue: islandora-connector-ocr + event: 'Generate Derivative' + source_term_uri: 'http://pcdm.org/use#OriginalFile' + derivative_term_uri: 'http://pcdm.org/use#ExtractedText' + mimetype: text/plain + args: null + destination_media_type: extracted_text + scheme: public + path: '[date:custom:Y]-[date:custom:m]/[node:nid]-[term:name].txt' diff --git a/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.features.yml b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.features.yml new file mode 100644 index 00000000..9e48e9fd --- /dev/null +++ b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.features.yml @@ -0,0 +1,2 @@ +bundle: islandora +required: true diff --git a/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml new file mode 100644 index 00000000..5081bb27 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml @@ -0,0 +1,16 @@ +name: 'Islandora Text Extraction Defaults' +type: module +description: 'Default config for the Islandora Text Extraction module.' +core: 8.x +package: Islandora +dependencies: + - field + - file + - islandora_core_feature + - islandora_text_extraction + - language + - media + - path + - system + - text + - user diff --git a/modules/islandora_video/LICENSE b/modules/islandora_video/LICENSE new file mode 100644 index 00000000..ecbc0593 --- /dev/null +++ b/modules/islandora_video/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/modules/islandora_video/README.md b/modules/islandora_video/README.md new file mode 100644 index 00000000..9d12b386 --- /dev/null +++ b/modules/islandora_video/README.md @@ -0,0 +1,23 @@ +# Islandora Video +[![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) + +## Introduction + +Islandora Video module for Drupal 8.2.x + +## Maintainers + +Current maintainers: + +* [Danny Lamb](https://github.com/dannylamb) + +## Development + +If you would like to contribute, please get involved by attending our weekly [Tech Call](https://github.com/Islandora-CLAW/CLAW/wiki). 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 Licencse 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. + +## License + +[GPLv2](http://www.gnu.org/licenses/gpl-2.0.txt) diff --git a/modules/islandora_video/config/schema/islandora_video.info.yml b/modules/islandora_video/config/schema/islandora_video.info.yml new file mode 100644 index 00000000..2d54e4b0 --- /dev/null +++ b/modules/islandora_video/config/schema/islandora_video.info.yml @@ -0,0 +1,28 @@ +action.configuration.generate_video_derivative: + type: mapping + label: 'Generate a video derivative...' + mapping: + queue: + type: text + label: 'Queue' + event: + type: text + label: 'Event Type' + source_term_uri: + type: text + label: 'Source term uri' + derivative_term_uri: + type: text + label: 'Destination term uri' + mimetype: + type: text + label: 'Video Mimetype' + args: + type: text + label: 'FFMpeg Arguments' + scheme: + type: text + label: 'Flysystem scheme' + path: + type: text + label: 'File path with extension' diff --git a/modules/islandora_video/islandora_video.info.yml b/modules/islandora_video/islandora_video.info.yml new file mode 100644 index 00000000..bccc6c80 --- /dev/null +++ b/modules/islandora_video/islandora_video.info.yml @@ -0,0 +1,7 @@ +name: 'Islandora Video' +description: 'Islandora video derivative actions' +type: module +package: Islandora +core: 8.x +dependencies: + - islandora diff --git a/modules/islandora_video/islandora_video.module b/modules/islandora_video/islandora_video.module new file mode 100644 index 00000000..d45baf06 --- /dev/null +++ b/modules/islandora_video/islandora_video.module @@ -0,0 +1,32 @@ +' . t('About') . ''; + $output .= '

' . t('Islandora Video adds video derivative actions.') . '

'; + $output .= '

' . t('These can be used with microservices to automatically generate derivative videos.') . '

'; + return $output; + + default: + } +} diff --git a/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php b/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php new file mode 100644 index 00000000..c28ffdc0 --- /dev/null +++ b/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php @@ -0,0 +1,55 @@ +getValue('mimetype')); + if ($exploded_mime[0] != 'video') { + $form_state->setErrorByName( + 'mimetype', + t('Please enter a video mimetype (e.g. video/mp4, video/quicktime, etc...)') + ); + } + } + +} diff --git a/modules/islandora_video/tests/fixtures/test_file.txt b/modules/islandora_video/tests/fixtures/test_file.txt new file mode 100644 index 00000000..e60b7106 --- /dev/null +++ b/modules/islandora_video/tests/fixtures/test_file.txt @@ -0,0 +1 @@ +THIS IS A TEST diff --git a/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php b/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php new file mode 100644 index 00000000..02beca06 --- /dev/null +++ b/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php @@ -0,0 +1,85 @@ +drupalCreateUser([ + 'bypass node access', + 'administer contexts', + 'administer actions', + 'view media', + 'create media', + 'update media', + ]); + $this->drupalLogin($account); + + // Create an action to generate a jpeg thumbnail. + $this->drupalGet('admin/config/system/actions'); + $this->getSession()->getPage()->findById("edit-action")->selectOption("Generate a video derivative"); + $this->getSession()->getPage()->pressButton(t('Create')); + $this->assertSession()->statusCodeEquals(200); + + $this->getSession()->getPage()->fillField('edit-label', "Generate video test derivative"); + $this->getSession()->getPage()->fillField('edit-id', "generate_video_test_derivative"); + $this->getSession()->getPage()->fillField('edit-queue', "generate-video-test-derivative"); + $this->getSession()->getPage()->fillField("edit-source-term", $this->preservationMasterTerm->label()); + $this->getSession()->getPage()->fillField("edit-derivative-term", $this->serviceFileTerm->label()); + $this->getSession()->getPage()->fillField('edit-mimetype', "video/mp4"); + $this->getSession()->getPage()->fillField('edit-args', "-f mp4"); + $this->getSession()->getPage()->fillField('edit-scheme', "public"); + $this->getSession()->getPage()->fillField('edit-path', "derp.mov"); + $this->getSession()->getPage()->pressButton(t('Save')); + $this->assertSession()->statusCodeEquals(200); + + // Create a context and add the action as a derivative reaction. + $this->createContext('Test', 'test'); + $this->addPresetReaction('test', 'derivative', "generate_video_test_derivative"); + $this->assertSession()->statusCodeEquals(200); + + // Create a new preservation master belonging to the node. + $values = [ + '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', + ]; + $this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, t('Save')); + + $expected = [ + 'source_uri' => 'test_file.txt', + 'destination_uri' => "node/1/media/{$this->testMediaType->id()}/3", + 'file_upload_uri' => 'public://derp.mov', + 'mimetype' => 'video/mp4', + 'args' => '-f mp4', + 'queue' => 'islandora-connector-homarus', + ]; + + // Check the message gets published and is of the right shape. + $this->checkMessage($expected); + } + +} diff --git a/src/Commands/IslandoraCommands.php b/src/Commands/IslandoraCommands.php new file mode 100644 index 00000000..19df5869 --- /dev/null +++ b/src/Commands/IslandoraCommands.php @@ -0,0 +1,82 @@ + self::REQ]) { + } + + /** + * Validate the provided userid. + * + * @hook validate migrate:import + */ + public function validateUser(CommandData $commandData) { + $userid = $commandData->input()->getOption('userid'); + if ($userid) { + $account = User::load($userid); + if (!$account) { + throw new \Exception("User ID does not match an existing user."); + } + } + } + + /** + * Switch the active user account to perform the import. + * + * @hook pre-command migrate:import + */ + public function preImport(CommandData $commandData) { + $userid = $commandData->input()->getOption('userid'); + if ($userid) { + $account = User::load($userid); + $accountSwitcher = \Drupal::service('account_switcher'); + $userSession = new UserSession([ + 'uid' => $account->id(), + 'name' => $account->getUsername(), + 'roles' => $account->getRoles(), + ]); + $accountSwitcher->switchTo($userSession); + $this->logger()->notice( + dt( + 'Now acting as user ID @id', + ['@id' => \Drupal::currentUser()->id()] + ) + ); + } + } + + /** + * Switch the user back once the migration is complete. + * + * @hook post-command migrate:import + */ + public function postImport($result, CommandData $commandData) { + if ($commandData->input()->getOption('userid')) { + $accountSwitcher = \Drupal::service('account_switcher'); + $this->logger()->notice(dt( + 'Switching back from user @uid.', + ['@uid' => \Drupal::currentUser()->id()] + )); + $accountSwitcher->switchBack(); + } + } + +} diff --git a/src/ContextReaction/NormalizerAlterReaction.php b/src/ContextReaction/NormalizerAlterReaction.php index 3b7f807b..1dcd5c68 100644 --- a/src/ContextReaction/NormalizerAlterReaction.php +++ b/src/ContextReaction/NormalizerAlterReaction.php @@ -3,7 +3,12 @@ namespace Drupal\islandora\ContextReaction; use Drupal\context\ContextReactionPluginBase; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\jsonld\Form\JsonLdSettingsForm; +use Drupal\islandora\IslandoraUtils; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base class to alter the normalizes Json-ld. @@ -12,10 +17,51 @@ use Drupal\Core\Entity\EntityInterface; * * @package Drupal\islandora\ContextReaction */ -abstract class NormalizerAlterReaction extends ContextReactionPluginBase { +abstract class NormalizerAlterReaction extends ContextReactionPluginBase implements ContainerFactoryPluginInterface { /** - * This reaction takes can alter the array of json-ld built from the entity. + * The configuration. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + protected $jsonldConfig; + + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, + $plugin_id, + $plugin_definition, + ConfigFactoryInterface $config_factory, + IslandoraUtils $utils) { + + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->jsonldConfig = $config_factory->get(JsonLdSettingsForm::CONFIG_NAME); + $this->utils = $utils; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('config.factory'), + $container->get('islandora.utils') + ); + } + + /** + * This reaction can alter the array of json-ld built from the entity. * * @param \Drupal\Core\Entity\EntityInterface|null $entity * The entity we are normalizing. @@ -26,4 +72,21 @@ abstract class NormalizerAlterReaction extends ContextReactionPluginBase { */ abstract public function execute(EntityInterface $entity = NULL, array &$normalized = NULL, array $context = NULL); + /** + * Helper function to get the url for an entity that repsects jsonld config. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return string + * The url. + */ + protected function getSubjectUrl(EntityInterface $entity) { + $format = ''; + if (!$this->jsonldConfig->get(JsonLdSettingsForm::REMOVE_JSONLD_FORMAT)) { + $format = 'jsonld'; + } + return $this->utils->getRestUrl($entity, $format); + } + } diff --git a/src/Controller/ManageMediaController.php b/src/Controller/ManageMediaController.php index 8ef77dff..776a6808 100644 --- a/src/Controller/ManageMediaController.php +++ b/src/Controller/ManageMediaController.php @@ -2,6 +2,9 @@ namespace Drupal\islandora\Controller; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Routing\RouteMatch; +use Drupal\node\Entity\Node; use Drupal\node\NodeInterface; /** @@ -14,6 +17,9 @@ class ManageMediaController extends ManageMembersController { * * @param \Drupal\node\NodeInterface $node * Node you want to add a media to. + * + * @return array + * Array of media types to add. */ public function addToNodePage(NodeInterface $node) { return $this->generateTypeList( @@ -26,4 +32,26 @@ class ManageMediaController extends ManageMembersController { ); } + /** + * Check if the object being displayed "is Islandora". + * + * @param \Drupal\Core\Routing\RouteMatch $route_match + * The current routing match. + * + * @return \Drupal\Core\Access\AccessResultAllowed|\Drupal\Core\Access\AccessResultForbidden + * Whether we can or can't show the "thing". + */ + public function access(RouteMatch $route_match) { + if ($route_match->getParameters()->has('node')) { + $node = $route_match->getParameter('node'); + if (!$node instanceof NodeInterface) { + $node = Node::load($node); + } + if ($node->hasField('field_model') && $node->hasField('field_member_of')) { + return AccessResult::allowed(); + } + } + return AccessResult::forbidden(); + } + } diff --git a/src/Controller/ManageMembersController.php b/src/Controller/ManageMembersController.php index 854e09cc..7f54706c 100644 --- a/src/Controller/ManageMembersController.php +++ b/src/Controller/ManageMembersController.php @@ -115,7 +115,7 @@ class ManageMembersController extends EntityController { $bundle->label(), $entity_add_form, [$bundle_type => $bundle->id()], - ['query' => ["edit[$field]" => $node->id()]] + ['query' => ["edit[$field][widget][0][target_id]" => $node->id()]] ), ]; } diff --git a/src/Controller/MediaSourceController.php b/src/Controller/MediaSourceController.php index 28377ddc..2e5b4b26 100644 --- a/src/Controller/MediaSourceController.php +++ b/src/Controller/MediaSourceController.php @@ -11,6 +11,7 @@ use Drupal\media\MediaInterface; use Drupal\media\MediaTypeInterface; use Drupal\node\NodeInterface; use Drupal\taxonomy\TermInterface; +use Drupal\islandora\IslandoraUtils; use Drupal\islandora\MediaSource\MediaSourceService; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -39,6 +40,13 @@ class MediaSourceController extends ControllerBase { */ protected $database; + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + /** * MediaSourceController constructor. * @@ -46,13 +54,17 @@ class MediaSourceController extends ControllerBase { * Service for business logic. * @param \Drupal\Core\Database\Connection $database * Database connection. + * @param \Drupal\islandora\IslandoraUtils $utils + * Islandora utils. */ public function __construct( MediaSourceService $service, - Connection $database + Connection $database, + IslandoraUtils $utils ) { $this->service = $service; $this->database = $database; + $this->utils = $utils; } /** @@ -67,7 +79,8 @@ class MediaSourceController extends ControllerBase { public static function create(ContainerInterface $container) { return new static( $container->get('islandora.media_source_service'), - $container->get('database') + $container->get('database'), + $container->get('islandora.utils') ); } @@ -162,7 +175,7 @@ class MediaSourceController extends ControllerBase { // We return the media if it was newly created. if ($media) { $response = new Response("", 201); - $response->headers->set("Location", $media->url('canonical', ['absolute' => TRUE])); + $response->headers->set("Location", $this->utils->getEntityUrl($media)); } else { $response = new Response("", 204); diff --git a/src/EventGenerator/EventGenerator.php b/src/EventGenerator/EventGenerator.php index edc32183..bf95e97d 100644 --- a/src/EventGenerator/EventGenerator.php +++ b/src/EventGenerator/EventGenerator.php @@ -3,8 +3,8 @@ namespace Drupal\islandora\EventGenerator; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Url; -use Drupal\file\FileInterface; +use Drupal\islandora\IslandoraUtils; +use Drupal\islandora\MediaSource\MediaSourceService; use Drupal\user\UserInterface; /** @@ -14,19 +14,48 @@ use Drupal\user\UserInterface; */ class EventGenerator implements EventGeneratorInterface { + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + + /** + * Media source service. + * + * @var \Drupal\islandora\MediaSource\MediaSourceService + */ + protected $mediaSource; + + /** + * Constructor. + * + * @param \Drupal\islandora\IslandoraUtils $utils + * Islandora utils. + * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source + * Media source service. + */ + public function __construct(IslandoraUtils $utils, MediaSourceService $media_source) { + $this->utils = $utils; + $this->mediaSource = $media_source; + } + /** * {@inheritdoc} */ public function generateEvent(EntityInterface $entity, UserInterface $user, array $data) { - $user_url = $user->toUrl()->setAbsolute()->toString(); + $user_url = $this->utils->getEntityUrl($user); - if ($entity instanceof FileInterface) { - $entity_url = $entity->url(); + $entity_type = $entity->getEntityTypeId(); + + if ($entity_type == 'file') { + $entity_url = $this->utils->getDownloadUrl($entity); $mimetype = $entity->getMimeType(); } else { - $entity_url = $entity->toUrl()->setAbsolute()->toString(); + $entity_url = $this->utils->getEntityUrl($entity); $mimetype = 'text/html'; } @@ -75,19 +104,33 @@ class EventGenerator implements EventGeneratorInterface { $event['object']['url'][] = [ "name" => "JSON", "type" => "Link", - "href" => "$entity_url?_format=json", + "href" => $this->utils->getRestUrl($entity, 'json'), "mediaType" => "application/json", "rel" => "alternate", ]; $event['object']['url'][] = [ "name" => "JSONLD", "type" => "Link", - "href" => "$entity_url?_format=jsonld", + "href" => $this->utils->getRestUrl($entity, 'jsonld'), "mediaType" => "application/ld+json", "rel" => "alternate", ]; } + // Add a link to the file described by a media. + if ($entity_type == 'media') { + $file = $this->mediaSource->getSourceFile($entity); + if ($file) { + $event['object']['url'][] = [ + "name" => "Describes", + "type" => "Link", + "href" => $this->utils->getDownloadUrl($file), + "mediaType" => $file->getMimeType(), + "rel" => "describes", + ]; + } + } + unset($data["event"]); unset($data["queue"]); diff --git a/src/EventSubscriber/AdminViewsRouteSubscriber.php b/src/EventSubscriber/AdminViewsRouteSubscriber.php index 2d7eb95b..64367673 100644 --- a/src/EventSubscriber/AdminViewsRouteSubscriber.php +++ b/src/EventSubscriber/AdminViewsRouteSubscriber.php @@ -16,9 +16,17 @@ class AdminViewsRouteSubscriber extends RouteSubscriberBase { protected function alterRoutes(RouteCollection $collection) { if ($route = $collection->get('view.media_of.page_1')) { $route->setOption('_admin_route', 'TRUE'); + $route->setRequirement('_permission', 'manage media'); + $route->setRequirement('_custom_access', '\Drupal\islandora\Controller\ManageMediaController::access'); } if ($route = $collection->get('view.manage_members.page_1')) { $route->setOption('_admin_route', 'TRUE'); + $route->setRequirement('_permission', 'manage members'); + $route->setRequirement('_custom_access', '\Drupal\islandora\Controller\ManageMediaController::access'); + } + if ($route = $collection->get('view.reorder_children.page_1')) { + $route->setOption('_admin_route', 'TRUE'); + $route->setRequirement('_permission', 'manage members'); } } diff --git a/src/EventSubscriber/JwtEventSubscriber.php b/src/EventSubscriber/JwtEventSubscriber.php index aadc35fb..4ea049c4 100644 --- a/src/EventSubscriber/JwtEventSubscriber.php +++ b/src/EventSubscriber/JwtEventSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\islandora\EventSubscriber; +use Drupal\islandora\Form\IslandoraSettingsForm; use Drupal\jwt\Authentication\Event\JwtAuthValidateEvent; use Drupal\jwt\Authentication\Event\JwtAuthValidEvent; use Drupal\jwt\Authentication\Event\JwtAuthGenerateEvent; @@ -88,7 +89,10 @@ class JwtEventSubscriber implements EventSubscriberInterface { // Standard claims, validated at JWT validation time. $event->addClaim('iat', time()); - $event->addClaim('exp', strtotime('+2 hour')); + $expiry_setting = \Drupal::config(IslandoraSettingsForm::CONFIG_NAME) + ->get(IslandoraSettingsForm::JWT_EXPIRY); + $expiry = $expiry_setting ? $expiry_setting : '+2 hour'; + $event->addClaim('exp', strtotime($expiry)); $event->addClaim('webid', $this->currentUser->id()); $event->addClaim('iss', $base_secure_url); diff --git a/src/EventSubscriber/LinkHeaderSubscriber.php b/src/EventSubscriber/LinkHeaderSubscriber.php index 3ff64d4b..4edf7da4 100644 --- a/src/EventSubscriber/LinkHeaderSubscriber.php +++ b/src/EventSubscriber/LinkHeaderSubscriber.php @@ -8,7 +8,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManager; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Url; +use Drupal\islandora\IslandoraUtils; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -64,6 +64,13 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { */ protected $requestStack; + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + /** * Constructor. * @@ -79,6 +86,8 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { * The route match object. * @param Symfony\Component\HttpFoundation\RequestStack $request_stack * Request stack (for current request). + * @param \Drupal\islandora\IslandoraUtils $utils + * Islandora utils. */ public function __construct( EntityTypeManager $entity_type_manager, @@ -86,7 +95,8 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { AccessManagerInterface $access_manager, AccountInterface $account, RouteMatchInterface $route_match, - RequestStack $request_stack + RequestStack $request_stack, + IslandoraUtils $utils ) { $this->entityTypeManager = $entity_type_manager; $this->entityFieldManager = $entity_field_manager; @@ -96,6 +106,7 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { $this->accessManager = $access_manager; $this->account = $account; $this->requestStack = $request_stack; + $this->utils = $utils; } /** @@ -194,6 +205,8 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { // Headers are subject to an access check. if ($referencedEntity->access('view')) { + $entity_url = $this->utils->getEntityUrl($referencedEntity); + // Taxonomy terms are written out as // ; rel="tag"; title="Tag Name" // where url is defined in field_same_as. @@ -201,7 +214,6 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { // it becomes the taxonomy term's local uri. if ($referencedEntity->getEntityTypeId() == 'taxonomy_term') { $rel = "tag"; - $entity_url = $referencedEntity->url('canonical', ['absolute' => TRUE]); if ($referencedEntity->hasField('field_external_uri')) { $external_uri = $referencedEntity->get('field_external_uri')->getValue(); if (!empty($external_uri) && isset($external_uri[0]['uri'])) { @@ -216,7 +228,6 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { // ; rel="related"; title="Field Label" // and the url is the local uri. $rel = "related"; - $entity_url = $referencedEntity->url('canonical', ['absolute' => TRUE]); $title = $field_definition->label(); } $links[] = "<$entity_url>; rel=\"$rel\"; title=\"$title\""; @@ -275,19 +286,16 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { continue; } + // Skip route if the user doesn't have access. $meta_route_name = "rest.entity.$entity_type.GET"; - $route_params = [$entity_type => $entity->id()]; - if (!$this->accessManager->checkNamedRoute($meta_route_name, $route_params, $this->account)) { continue; } - $meta_url = Url::fromRoute($meta_route_name, $route_params) - ->setAbsolute() - ->toString(); + $meta_url = $this->utils->getRestUrl($entity, $format); - $links[] = "<$meta_url?_format=$format>; rel=\"alternate\"; type=\"$mime\""; + $links[] = "<$meta_url>; rel=\"alternate\"; type=\"$mime\""; } } diff --git a/src/EventSubscriber/MediaLinkHeaderSubscriber.php b/src/EventSubscriber/MediaLinkHeaderSubscriber.php index 2c4a6fe3..5df879f0 100644 --- a/src/EventSubscriber/MediaLinkHeaderSubscriber.php +++ b/src/EventSubscriber/MediaLinkHeaderSubscriber.php @@ -80,7 +80,7 @@ class MediaLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSub // Collect file links for the media. foreach ($media->get($source_field)->referencedEntities() as $referencedEntity) { if ($referencedEntity->access('view')) { - $file_url = $referencedEntity->url('canonical', ['absolute' => TRUE]); + $file_url = $this->utils->getDownloadUrl($referencedEntity); $links[] = "<$file_url>; rel=\"describes\"; type=\"{$referencedEntity->getMimeType()}\""; } } diff --git a/src/EventSubscriber/NodeLinkHeaderSubscriber.php b/src/EventSubscriber/NodeLinkHeaderSubscriber.php index 6ce958b4..443ce86f 100644 --- a/src/EventSubscriber/NodeLinkHeaderSubscriber.php +++ b/src/EventSubscriber/NodeLinkHeaderSubscriber.php @@ -2,15 +2,8 @@ namespace Drupal\islandora\EventSubscriber; -use Drupal\Core\Access\AccessManagerInterface; -use Drupal\Core\Entity\EntityFieldManager; -use Drupal\Core\Entity\EntityTypeManager; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\islandora\IslandoraUtils; use Drupal\node\NodeInterface; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -20,51 +13,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; */ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubscriberInterface { - /** - * Derivative utils. - * - * @var \Drupal\islandora\IslandoraUtils - */ - protected $utils; - - /** - * Constructor. - * - * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager - * The entity field manager. - * @param \Drupal\Core\Access\AccessManagerInterface $access_manager - * The access manager. - * @param \Drupal\Core\Session\AccountInterface $account - * The current user. - * @param \Drupal\Core\Routing\RouteMatchInterface $route_match - * The route match object. - * @param Symfony\Component\HttpFoundation\RequestStack $request_stack - * Request stack (for current request). - * @param \Drupal\islandora\IslandoraUtils $utils - * Derivative utils. - */ - public function __construct( - EntityTypeManager $entity_type_manager, - EntityFieldManager $entity_field_manager, - AccessManagerInterface $access_manager, - AccountInterface $account, - RouteMatchInterface $route_match, - RequestStack $request_stack, - IslandoraUtils $utils - ) { - $this->entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_manager; - $this->accessManager = $access_manager; - $this->account = $account; - $this->routeMatch = $route_match; - $this->accessManager = $access_manager; - $this->account = $account; - $this->requestStack = $request_stack; - $this->utils = $utils; - } - /** * Adds node-specific link headers to appropriate responses. * @@ -95,12 +43,12 @@ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubs } /** - * Generates link headrs for media asssociated with a node. + * Generates link headers for media associated with a node. */ protected function generateRelatedMediaLinks(NodeInterface $node) { $links = []; foreach ($this->utils->getMedia($node) as $media) { - $url = $media->url('canonical', ['absolute' => TRUE]); + $url = $this->utils->getEntityUrl($media); foreach ($media->referencedEntities() as $term) { if ($term->getEntityTypeId() == 'taxonomy_term' && $term->hasField('field_external_uri')) { $field = $term->get('field_external_uri'); diff --git a/src/Flysystem/Adapter/FedoraAdapter.php b/src/Flysystem/Adapter/FedoraAdapter.php index 03900753..4aebde6e 100644 --- a/src/Flysystem/Adapter/FedoraAdapter.php +++ b/src/Flysystem/Adapter/FedoraAdapter.php @@ -20,7 +20,18 @@ class FedoraAdapter implements AdapterInterface { use StreamedCopyTrait; use NotSupportingVisibilityTrait; + /** + * Fedora client. + * + * @var \Islandora\Chullo\IFedoraApi + */ protected $fedora; + + /** + * Mimetype guesser. + * + * @var \Symfony\Component\HttpFoundation\File\Mimetype\MimeTypeGuesserInterface + */ protected $mimeTypeGuesser; /** @@ -75,7 +86,6 @@ class FedoraAdapter implements AdapterInterface { $meta = $this->getMetadataFromHeaders($response); $meta['path'] = $path; - if ($meta['type'] == 'file') { $meta['stream'] = StreamWrapper::getResource($response->getBody()); } diff --git a/src/Flysystem/Fedora.php b/src/Flysystem/Fedora.php index 772e89e6..b90fc529 100644 --- a/src/Flysystem/Fedora.php +++ b/src/Flysystem/Fedora.php @@ -2,11 +2,14 @@ namespace Drupal\islandora\Flysystem; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Url; use Drupal\flysystem\Plugin\FlysystemPluginInterface; use Drupal\flysystem\Plugin\FlysystemUrlTrait; use Drupal\islandora\Flysystem\Adapter\FedoraAdapter; +use Drupal\jwt\Authentication\Provider\JwtAuth; use GuzzleHttp\HandlerStack; use GuzzleHttp\Client; use Islandora\Chullo\IFedoraApi; @@ -24,10 +27,27 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac use FlysystemUrlTrait; + /** + * Fedora client. + * + * @var \Islandora\Chullo\IFedoraApi + */ protected $fedora; + /** + * Mimetype guesser. + * + * @var \Symfony\Component\HttpFoundation\File\Mimetype\MimeTypeGuesserInterface + */ protected $mimeTypeGuesser; + /** + * Language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + /** * Constructs a Fedora plugin for Flysystem. * @@ -35,26 +55,27 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac * Fedora client. * @param \Symfony\Component\HttpFoundation\File\Mimetype\MimeTypeGuesserInterface $mime_type_guesser * Mimetype guesser. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * Language manager. */ public function __construct( IFedoraApi $fedora, - MimeTypeGuesserInterface $mime_type_guesser + MimeTypeGuesserInterface $mime_type_guesser, + LanguageManagerInterface $language_manager ) { $this->fedora = $fedora; $this->mimeTypeGuesser = $mime_type_guesser; + $this->languageManager = $language_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - // Construct Authorization header using jwt token. - $jwt = $container->get('jwt.authentication.jwt'); - $auth = 'Bearer ' . $jwt->generateToken(); - // Construct guzzle client to middleware that adds the header. + // Construct guzzle client to middleware that adds JWT. $stack = HandlerStack::create(); - $stack->push(static::addHeader('Authorization', $auth)); + $stack->push(static::addJwt($container->get('jwt.authentication.jwt'))); $client = new Client([ 'handler' => $stack, 'base_uri' => $configuration['root'], @@ -64,29 +85,27 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac // Return it. return new static( $fedora, - $container->get('file.mime_type.guesser') + $container->get('file.mime_type.guesser'), + $container->get('language_manager') ); } /** * Guzzle middleware to add a header to outgoing requests. * - * @param string $header - * Header name. - * @param string $value - * Header value. + * @param \Drupal\jwt\Authentication\Provider\JwtAuth $jwt + * JWT. */ - public static function addHeader($header, $value) { - return function (callable $handler) use ($header, $value) { + public static function addJwt(JwtAuth $jwt) { + return function (callable $handler) use ($jwt) { return function ( RequestInterface $request, array $options ) use ( -$handler, - $header, - $value -) { - $request = $request->withHeader($header, $value); + $handler, + $jwt + ) { + $request = $request->withHeader('Authorization', 'Bearer ' . $jwt->generateToken()); return $handler($request, $options); }; }; @@ -121,4 +140,24 @@ $handler, return []; } + /** + * {@inheritdoc} + */ + public function getExternalUrl($uri) { + $path = str_replace('\\', '/', $this->getTarget($uri)); + + $arguments = [ + 'scheme' => $this->getScheme($uri), + 'filepath' => $path, + ]; + + // Force file urls to be language neutral. + $undefined = $this->languageManager->getLanguage('und'); + return Url::fromRoute( + 'flysystem.serve', + $arguments, + ['absolute' => TRUE, 'language' => $undefined] + )->toString(); + } + } diff --git a/src/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php index 60859782..8079279d 100644 --- a/src/Form/IslandoraSettingsForm.php +++ b/src/Form/IslandoraSettingsForm.php @@ -2,11 +2,17 @@ namespace Drupal\islandora\Form; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeBundleInfo; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; +use GuzzleHttp\Exception\ConnectException; +use Islandora\Crayfish\Commons\Client\GeminiClient; use Stomp\Client; use Stomp\Exception\StompException; use Stomp\StatefulStomp; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Config form for Islandora settings. @@ -15,6 +21,39 @@ class IslandoraSettingsForm extends ConfigFormBase { const CONFIG_NAME = 'islandora.settings'; const BROKER_URL = 'broker_url'; + const JWT_EXPIRY = 'jwt_expiry'; + const GEMINI_URL = 'gemini_url'; + const GEMINI_PSEUDO = 'gemini_pseudo_bundles'; + + /** + * To list the available bundle types. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfo + */ + private $entityTypeBundleInfo; + + /** + * Constructs a \Drupal\system\ConfigFormBase object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\Core\Entity\EntityTypeBundleInfo $entity_type_bundle_info + * The EntityTypeBundleInfo service. + */ + public function __construct(ConfigFactoryInterface $config_factory, EntityTypeBundleInfo $entity_type_bundle_info) { + $this->setConfigFactory($config_factory); + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('entity_type.bundle.info') + ); + } /** * {@inheritdoc} @@ -41,9 +80,46 @@ class IslandoraSettingsForm extends ConfigFormBase { $form[self::BROKER_URL] = [ '#type' => 'textfield', '#title' => $this->t('Broker URL'), - '#default_value' => $config->get(self::BROKER_URL) ? $config->get(self::BROKER_URL) : 'tcp://localhost:61613', + '#default_value' => $config->get(self::BROKER_URL), + ]; + + $form[self::JWT_EXPIRY] = [ + '#type' => 'textfield', + '#title' => $this->t('JWT Expiry'), + '#default_value' => $config->get(self::JWT_EXPIRY), + ]; + + $form[self::GEMINI_URL] = [ + '#type' => 'textfield', + '#title' => $this->t('Gemini URL'), + '#default_value' => $config->get(self::GEMINI_URL), ]; + $selected_bundles = $config->get(self::GEMINI_PSEUDO); + + $options = []; + foreach (['node', 'media', 'taxonomy_term'] as $content_entity) { + $bundles = $this->entityTypeBundleInfo->getBundleInfo($content_entity); + foreach ($bundles as $bundle => $bundle_properties) { + $options["{$bundle}:{$content_entity}"] = + $this->t('@label (@type)', [ + '@label' => $bundle_properties['label'], + '@type' => $content_entity, + ]); + } + } + + $form['bundle_container'] = [ + '#type' => 'details', + '#title' => $this->t('Bundles with Gemini URI Pseudo field'), + '#description' => $this->t('The selected bundles can display the pseudo-field showing the Gemini linked URI. Configured in the field display.'), + '#open' => TRUE, + self::GEMINI_PSEUDO => [ + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => $selected_bundles, + ], + ]; return parent::buildForm($form, $form_state); } @@ -74,16 +150,73 @@ class IslandoraSettingsForm extends ConfigFormBase { ) ); } + + // Validate jwt expiry as a valid time string. + $expiry = $form_state->getValue(self::JWT_EXPIRY); + if (strtotime($expiry) === FALSE) { + $form_state->setErrorByName( + self::JWT_EXPIRY, + $this->t( + '"@expiry" is not a valid time or interval expression.', + ['@expiry' => $expiry] + ) + ); + } + + // Needed for the elseif below. + $pseudo_types = array_filter($form_state->getValue(self::GEMINI_PSEUDO)); + + // Validate Gemini URL by validating the URL. + $geminiUrlValue = trim($form_state->getValue(self::GEMINI_URL)); + if (!empty($geminiUrlValue)) { + try { + $geminiUrl = Url::fromUri($geminiUrlValue); + $client = GeminiClient::create($geminiUrlValue, $this->logger('islandora')); + $client->findByUri('http://example.org'); + } + // Uri is invalid. + catch (\InvalidArgumentException $e) { + $form_state->setErrorByName( + self::GEMINI_URL, + $this->t( + 'Cannot parse URL @url', + ['@url' => $geminiUrlValue] + ) + ); + } + // Uri is not available. + catch (ConnectException $e) { + $form_state->setErrorByName( + self::GEMINI_URL, + $this->t( + 'Cannot connect to URL @url', + ['@url' => $geminiUrlValue] + ) + ); + } + } + elseif (count($pseudo_types) > 0) { + $form_state->setErrorByName( + self::GEMINI_URL, + $this->t('Must enter Gemini URL before selecting bundles to display a pseudo field on.') + ); + } + } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $config = \Drupal::service('config.factory')->getEditable(self::CONFIG_NAME); + $config = $this->configFactory->getEditable(self::CONFIG_NAME); + + $pseudo_types = array_filter($form_state->getValue(self::GEMINI_PSEUDO)); $config ->set(self::BROKER_URL, $form_state->getValue(self::BROKER_URL)) + ->set(self::JWT_EXPIRY, $form_state->getValue(self::JWT_EXPIRY)) + ->set(self::GEMINI_URL, $form_state->getValue(self::GEMINI_URL)) + ->set(self::GEMINI_PSEUDO, $pseudo_types) ->save(); parent::submitForm($form, $form_state); diff --git a/src/GeminiClientFactory.php b/src/GeminiClientFactory.php new file mode 100644 index 00000000..5a8ccd5b --- /dev/null +++ b/src/GeminiClientFactory.php @@ -0,0 +1,47 @@ +get(IslandoraSettingsForm::CONFIG_NAME); + $geminiUrl = $settings->get(IslandoraSettingsForm::GEMINI_URL); + + // Only attempt if there is one. + if (!empty($geminiUrl)) { + return GeminiClient::create($geminiUrl, $logger); + } + else { + $logger->notice("Attempted to create Gemini client without a Gemini URL defined."); + throw new PreconditionFailedHttpException("Unable to instantiate GeminiClient, missing Gemini URI in Islandora setting."); + } + } + +} diff --git a/src/GeminiLookup.php b/src/GeminiLookup.php new file mode 100644 index 00000000..20bd42e0 --- /dev/null +++ b/src/GeminiLookup.php @@ -0,0 +1,162 @@ +geminiClient = $client; + $this->jwtProvider = $jwt_auth; + $this->mediaSource = $media_source; + $this->guzzle = $guzzle; + $this->logger = $logger; + } + + /** + * Lookup this entity's URI in the Gemini db and return the other URI. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to look for. + * + * @return string|null + * Return the URI or null + */ + public function lookup(EntityInterface $entity) { + // Exit early if the entity hasn't been saved yet. + if ($entity->id() == NULL) { + return NULL; + } + + $is_media = $entity->getEntityTypeId() == 'media'; + + // Use the entity's uuid unless it's a media, + // use its file's uuid instead. + if ($is_media) { + try { + $file = $this->mediaSource->getSourceFile($entity); + $uuid = $file->uuid(); + } + // If the media has no source file, exit early. + catch (NotFoundHttpException $e) { + return NULL; + } + } + else { + $uuid = $entity->uuid(); + } + + // Look it up in Gemini. + $token = "Bearer " . $this->jwtProvider->generateToken(); + $urls = $this->geminiClient->getUrls($uuid, $token); + + // Exit early if there's no results from Gemini. + if (empty($urls)) { + return NULL; + } + + // If it's not a media, just return the url from Gemini;. + if (!$is_media) { + return $urls['fedora']; + } + + // If it's a media, perform a HEAD request against + // the file in Fedora and get its 'describedy' link header. + try { + $head = $this->guzzle->head( + $urls['fedora'], + ['allow_redirects' => FALSE, 'headers' => ['Authorization' => $token]] + ); + $links = Psr7\parse_header($head->getHeader("Link")); + foreach ($links as $link) { + if ($link['rel'] == 'describedby') { + return trim($link[0], '<>'); + } + } + } + catch (RequestException $e) { + $this->logger->warn( + "Error performing Gemini lookup for media. Fedora HEAD to @url returned @status => @message", + [ + '@url' => $urls['fedora'], + '@status' => $e->getCode(), + '@message' => $e->getMessage, + ] + ); + return NULL; + } + + // Return null if no link header is found. + return NULL; + } + +} diff --git a/src/IslandoraUtils.php b/src/IslandoraUtils.php index e6ba1b75..e98766c7 100644 --- a/src/IslandoraUtils.php +++ b/src/IslandoraUtils.php @@ -4,12 +4,15 @@ namespace Drupal\islandora; use Drupal\context\ContextManager; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityFieldManager; use Drupal\Core\Entity\EntityTypeManager; use Drupal\Core\Entity\Query\QueryException; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Site\Settings; +use Drupal\Core\Url; use Drupal\file\FileInterface; use Drupal\flysystem\FlysystemFactory; use Drupal\islandora\ContextProvider\NodeContextProvider; @@ -64,6 +67,13 @@ class IslandoraUtils { */ protected $flysystemFactory; + /** + * Language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + /** * Constructor. * @@ -77,19 +87,23 @@ class IslandoraUtils { * Context manager. * @param \Drupal\flysystem\FlysystemFactory $flysystem_factory * Flysystem factory. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * Language manager. */ public function __construct( EntityTypeManager $entity_type_manager, EntityFieldManager $entity_field_manager, QueryFactory $entity_query, ContextManager $context_manager, - FlysystemFactory $flysystem_factory + FlysystemFactory $flysystem_factory, + LanguageManagerInterface $language_manager ) { $this->entityTypeManager = $entity_type_manager; $this->entityFieldManager = $entity_field_manager; $this->entityQuery = $entity_query; $this->contextManager = $context_manager; $this->flysystemFactory = $flysystem_factory; + $this->languageManager = $language_manager; } /** @@ -498,4 +512,62 @@ class IslandoraUtils { return $condition; } + /** + * Gets the id URL of an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose URL you want. + * + * @return string + * The entity URL. + */ + public function getEntityUrl(EntityInterface $entity) { + $undefined = $this->languageManager->getLanguage('und'); + $entity_type = $entity->getEntityTypeId(); + return Url::fromRoute( + "entity.$entity_type.canonical", + [$entity_type => $entity->id()], + ['absolute' => TRUE, 'language' => $undefined] + )->toString(); + } + + /** + * Gets the downloadable URL for a file. + * + * @param \Drupal\file\FileInterface $file + * The file whose URL you want. + * + * @return string + * The file URL. + */ + public function getDownloadUrl(FileInterface $file) { + $undefined = $this->languageManager->getLanguage('und'); + return $file->url('canonical', ['absolute' => TRUE, 'language' => $undefined]); + } + + /** + * Gets the URL for an entity's REST endpoint. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity whose REST endpoint you want. + * @param string $format + * REST serialization format. + * + * @return string + * The REST URL. + */ + public function getRestUrl(EntityInterface $entity, $format = '') { + $undefined = $this->languageManager->getLanguage('und'); + $entity_type = $entity->getEntityTypeId(); + $rest_url = Url::fromRoute( + "rest.entity.$entity_type.GET", + [$entity_type => $entity->id()], + ['absolute' => TRUE, 'language' => $undefined] + )->toString(); + if (!empty($format)) { + $rest_url .= "?_format=$format"; + } + return $rest_url; + } + } diff --git a/src/MediaSource/MediaSourceService.php b/src/MediaSource/MediaSourceService.php index 2044ab00..8c2477ad 100644 --- a/src/MediaSource/MediaSourceService.php +++ b/src/MediaSource/MediaSourceService.php @@ -155,7 +155,7 @@ class MediaSourceService { * @param string $mimetype * New mimetype of contents. * - * @throws HttpException + * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function updateSourceField( MediaInterface $media, @@ -206,6 +206,8 @@ class MediaSourceService { $content_length = stream_copy_to_stream($resource, $destination); + fclose($destination); + if ($content_length === FALSE) { throw new HttpException(500, "Request body could not be copied to $uri"); } @@ -240,7 +242,7 @@ class MediaSourceService { * @param string $content_location * Drupal/PHP stream wrapper for where to upload the binary. * - * @throws HttpException + * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function putToNode( NodeInterface $node, diff --git a/src/Plugin/Action/AbstractGenerateDerivative.php b/src/Plugin/Action/AbstractGenerateDerivative.php new file mode 100644 index 00000000..233ce774 --- /dev/null +++ b/src/Plugin/Action/AbstractGenerateDerivative.php @@ -0,0 +1,337 @@ +utils = $utils; + $this->mediaSource = $media_source; + $this->token = $token; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('islandora.eventgenerator'), + $container->get('islandora.stomp'), + $container->get('jwt.authentication.jwt'), + $container->get('islandora.utils'), + $container->get('islandora.media_source_service'), + $container->get('token') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'queue' => 'islandora-connector-houdini', + 'event' => 'Generate Derivative', + 'source_term_uri' => '', + 'derivative_term_uri' => '', + 'mimetype' => '', + 'args' => '', + 'destination_media_type' => '', + 'scheme' => file_default_scheme(), + 'path' => '[date:custom:Y]-[date:custom:m]/[node:nid].bin', + ]; + } + + /** + * Override this to return arbitrary data as an array to be json encoded. + */ + protected function generateData(EntityInterface $entity) { + $data = parent::generateData($entity); + + // Find media belonging to node that has the source term, and set its file + // url in the data array. + $source_term = $this->utils->getTermForUri($this->configuration['source_term_uri']); + if (!$source_term) { + throw new \RuntimeException("Could not locate source term with uri" . $this->configuration['source_term_uri'], 500); + } + + $source_media = $this->utils->getMediaWithTerm($entity, $source_term); + if (!$source_media) { + throw new \RuntimeException("Could not locate source media", 500); + } + + $source_file = $this->mediaSource->getSourceFile($source_media); + if (!$source_file) { + throw new \RuntimeException("Could not locate source file for media {$source_media->id()}", 500); + } + + $data['source_uri'] = $this->utils->getDownloadUrl($source_file); + + // Find the term for the derivative and use it to set the destination url + // in the data array. + $derivative_term = $this->utils->getTermForUri($this->configuration['derivative_term_uri']); + if (!$source_term) { + throw new \RuntimeException("Could not locate derivative term with uri" . $this->configuration['derivative_term_uri'], 500); + } + + $route_params = [ + 'node' => $entity->id(), + 'media_type' => $this->configuration['destination_media_type'], + 'taxonomy_term' => $derivative_term->id(), + ]; + $data['destination_uri'] = Url::fromRoute('islandora.media_source_put_to_node', $route_params) + ->setAbsolute() + ->toString(); + + $token_data = [ + 'node' => $entity, + 'media' => $source_media, + 'term' => $derivative_term, + ]; + $path = $this->token->replace($data['path'], $token_data); + $data['file_upload_uri'] = $data['scheme'] . '://' . $path; + + // Get rid of some config so we just pass along + // what the camel route and microservice need. + unset($data['source_term_uri']); + unset($data['derivative_term_uri']); + unset($data['path']); + unset($data['scheme']); + unset($data['destination_media_type']); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $schemes = $this->utils->getFilesystemSchemes(); + $scheme_options = array_combine($schemes, $schemes); + + $form = parent::buildConfigurationForm($form, $form_state); + $form['event']['#disabled'] = 'disabled'; + + $form['source_term'] = [ + '#type' => 'entity_autocomplete', + '#target_type' => 'taxonomy_term', + '#title' => t('Source term'), + '#default_value' => $this->utils->getTermForUri($this->configuration['source_term_uri']), + '#required' => TRUE, + '#description' => t('Term indicating the source media'), + ]; + $form['derivative_term'] = [ + '#type' => 'entity_autocomplete', + '#target_type' => 'taxonomy_term', + '#title' => t('Derivative term'), + '#default_value' => $this->utils->getTermForUri($this->configuration['derivative_term_uri']), + '#required' => TRUE, + '#description' => t('Term indicating the derivative media'), + ]; + $form['destination_media_type'] = [ + '#type' => 'entity_autocomplete', + '#target_type' => 'media_type', + '#title' => t('Derivative media type'), + '#default_value' => $this->getEntityById($this->configuration['destination_media_type']), + '#required' => TRUE, + '#description' => t('The Drupal media type to create with this derivative, can be different than the source'), + ]; + $form['mimetype'] = [ + '#type' => 'textfield', + '#title' => t('Mimetype'), + '#default_value' => $this->configuration['mimetype'], + '#required' => TRUE, + '#rows' => '8', + '#description' => t('Mimetype to convert to (e.g. image/jpeg, video/mp4, etc...)'), + ]; + $form['args'] = [ + '#type' => 'textfield', + '#title' => t('Additional arguments'), + '#default_value' => $this->configuration['args'], + '#rows' => '8', + '#description' => t('Additional command line arguments'), + ]; + $form['scheme'] = [ + '#type' => 'select', + '#title' => t('File system'), + '#options' => $scheme_options, + '#default_value' => $this->configuration['scheme'], + '#required' => TRUE, + ]; + $form['path'] = [ + '#type' => 'textfield', + '#title' => t('File path'), + '#default_value' => $this->configuration['path'], + '#description' => t('Path within the upload destination where files will be stored. Includes the filename and optional extension.'), + ]; + $form['queue'] = [ + '#type' => 'textfield', + '#title' => t('Queue name'), + '#default_value' => $this->configuration['queue'], + '#description' => t('Queue name to send along to help routing events, CHANGE WITH CARE. Defaults to :queue', [ + ':queue' => $this->defaultConfiguration()['queue'], + ]), + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $form_state); + + $exploded_mime = explode('/', $form_state->getValue('mimetype')); + + if (count($exploded_mime) != 2) { + $form_state->setErrorByName( + 'mimetype', + t('Please enter a mimetype (e.g. image/jpeg, video/mp4, audio/mp3, etc...)') + ); + } + + if (empty($exploded_mime[1])) { + $form_state->setErrorByName( + 'mimetype', + t('Please enter a mimetype (e.g. image/jpeg, video/mp4, audio/mp3, etc...)') + ); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + $tid = $form_state->getValue('source_term'); + $term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid); + $this->configuration['source_term_uri'] = $this->utils->getUriForTerm($term); + + $tid = $form_state->getValue('derivative_term'); + $term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid); + $this->configuration['derivative_term_uri'] = $this->utils->getUriForTerm($term); + + $this->configuration['mimetype'] = $form_state->getValue('mimetype'); + $this->configuration['args'] = $form_state->getValue('args'); + $this->configuration['scheme'] = $form_state->getValue('scheme'); + $this->configuration['path'] = trim($form_state->getValue('path'), '\\/'); + $this->configuration['destination_media_type'] = $form_state->getValue('destination_media_type'); + } + + /** + * Find a media_type by id and return it or nothing. + * + * @param string $entity_id + * The media type. + * + * @return \Drupal\Core\Entity\EntityInterface|string + * Return the loaded entity or nothing. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * Thrown by getStorage() if the entity type doesn't exist. + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * Thrown by getStorage() if the storage handler couldn't be loaded. + */ + protected function getEntityById($entity_id) { + $entity_ids = $this->entityTypeManager->getStorage('media_type') + ->getQuery()->condition('id', $entity_id)->execute(); + + $id = reset($entity_ids); + if ($id !== FALSE) { + return $this->entityTypeManager->getStorage('media_type')->load($id); + } + return ''; + } + +} diff --git a/src/Plugin/Action/DeleteMedia.php b/src/Plugin/Action/DeleteMedia.php deleted file mode 100644 index 82c4d842..00000000 --- a/src/Plugin/Action/DeleteMedia.php +++ /dev/null @@ -1,35 +0,0 @@ -delete(); - } - } - - /** - * {@inheritdoc} - */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - return $object->access('delete', $account, $return_as_object); - } - -} diff --git a/src/Plugin/Action/EmitFileEvent.php b/src/Plugin/Action/EmitFileEvent.php index 8f4ebe4c..c3e206f0 100644 --- a/src/Plugin/Action/EmitFileEvent.php +++ b/src/Plugin/Action/EmitFileEvent.php @@ -104,6 +104,11 @@ class EmitFileEvent extends EmitEvent { $data = parent::generateData($entity); if (isset($flysystem_config[$scheme]) && $flysystem_config[$scheme]['driver'] == 'fedora') { + // Fdora $uri for files may contain ':///' so we need to replace + // the three / with two. + if (strpos($uri, $scheme . ':///') !== FALSE) { + $uri = str_replace($scheme . ':///', $scheme . '://', $uri); + } $data['fedora_uri'] = str_replace("$scheme://", $flysystem_config[$scheme]['config']['root'], $uri); } return $data; diff --git a/src/Plugin/Condition/EntityBundle.php b/src/Plugin/Condition/EntityBundle.php new file mode 100644 index 00000000..77cf2826 --- /dev/null +++ b/src/Plugin/Condition/EntityBundle.php @@ -0,0 +1,97 @@ +getBundleInfo($content_entity); + foreach ($bundles as $bundle => $bundle_properties) { + $options[$bundle] = $this->t('@bundle (@type)', [ + '@bundle' => $bundle_properties['label'], + '@type' => $content_entity, + ]); + } + } + + $form['bundles'] = [ + '#title' => $this->t('Bundles'), + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => $this->configuration['bundles'], + ]; + + return parent::buildConfigurationForm($form, $form_state);; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['bundles'] = array_filter($form_state->getValue('bundles')); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + foreach ($this->getContexts() as $context) { + if ($context->hasContextValue()) { + $entity = $context->getContextValue(); + if (!empty($this->configuration['bundles'][$entity->bundle()])) { + return !$this->isNegated(); + } + } + } + return $this->isNegated(); + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (empty($this->configuration['bundles'])) { + return $this->t('No bundles are selected.'); + } + + return $this->t( + 'Entity bundle in the list: @bundles', + [ + '@bundles' => implode(', ', $this->configuration['field']), + ] + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array_merge( + ['bundles' => []], + parent::defaultConfiguration() + ); + } + +} diff --git a/src/Plugin/Condition/MediaHasMimetype.php b/src/Plugin/Condition/MediaHasMimetype.php new file mode 100644 index 00000000..fcf085ed --- /dev/null +++ b/src/Plugin/Condition/MediaHasMimetype.php @@ -0,0 +1,174 @@ +utils = $utils; + $this->entityTypeManager = $entity_type_manager; + $this->mediaSource = $media_source; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('islandora.utils'), + $container->get('entity_type.manager'), + $container->get('islandora.media_source_service') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['mimetypes'] = [ + '#type' => 'textfield', + '#title' => t('Mime types'), + '#default_value' => $this->configuration['mimetypes'], + '#required' => TRUE, + '#maxlength' => 256, + '#description' => t('Comma-delimited list of Mime types (e.g. image/jpeg, video/mp4, etc...) that trigger the condition.'), + ]; + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['mimetypes'] = $form_state->getValue('mimetypes'); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function summary() { + $mimetypes = $this->configuration['mimetypes']; + return $this->t( + 'The media has one of the Mime types @mimetypes', + [ + '@mimetypes' => $mimetypes, + ] + ); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + if (empty($this->configuration['mimetypes']) && !$this->isNegated()) { + return TRUE; + } + + $node = \Drupal::routeMatch()->getParameter('node'); + + if (is_null($node) || is_string($node)) { + return FALSE; + } + + $media = $this->utils->getMedia($node); + + if (count($media) > 0) { + $mimetypes = explode(',', str_replace(' ', '', $this->configuration['mimetypes'])); + foreach ($media as $medium) { + $file = $this->mediaSource->getSourceFile($medium); + if (in_array($file->getMimeType(), $mimetypes)) { + return $this->isNegated() ? FALSE : TRUE; + } + } + } + else { + return FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array_merge( + ['mimetypes' => ''], + parent::defaultConfiguration() + ); + } + +} diff --git a/src/Plugin/Condition/NodeHadNamespace.php b/src/Plugin/Condition/NodeHadNamespace.php new file mode 100644 index 00000000..a033913a --- /dev/null +++ b/src/Plugin/Condition/NodeHadNamespace.php @@ -0,0 +1,187 @@ +utils = $utils; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('islandora.utils'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['namespace'] = [ + '#type' => 'textfield', + '#title' => $this->t('Islandora 7.x Namespaces'), + '#description' => $this->t('Comma-delimited list of 7.x PID namespaces, including the trailing colon (e.g., "islandora:,ir:").'), + '#default_value' => $this->configuration['namespace'], + '#maxlength' => 256, + ]; + $field_map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('string'); + $node_fields = array_keys($field_map['node']); + $options = array_combine($node_fields, $node_fields); + $form['pid_field'] = [ + '#type' => 'select', + '#title' => t('Field that contains the PID'), + '#options' => $options, + '#default_value' => $this->configuration['pid_field'], + '#required' => TRUE, + '#description' => t("Machine name of the field that contains the PID."), + ]; + + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['namespace'] = NULL; + $namespace = $form_state->getValue('namespace'); + if (!empty($namespace)) { + if ($namespace) { + $this->configuration['namespace'] = $namespace; + } + } + $this->configuration['pid_field'] = $form_state->getValue('pid_field'); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + if (empty($this->configuration['namespace']) && !$this->isNegated()) { + return TRUE; + } + + $node = $this->getContextValue('node'); + if (!$node) { + return FALSE; + } + return $this->evaluateEntity($node); + } + + /** + * Evaluates if the value of field_pid with a registered 7.x namespace. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to evalute. + * + * @return bool + * TRUE if entity has the specified namespace, otherwise FALSE. + */ + protected function evaluateEntity(EntityInterface $entity) { + $pid_field = $this->configuration['pid_field']; + if ($entity->hasField($pid_field)) { + $pid_value = $entity->get($pid_field)->getValue(); + $pid = $pid_value[0]['value']; + $namespace = strtok($pid, ':') . ':'; + $registered_namespaces = explode(',', $this->configuration['namespace']); + foreach ($registered_namespaces as &$registered_namespace) { + $registered_namespace = trim($registered_namespace); + if (in_array($namespace, $registered_namespaces)) { + return $this->isNegated() ? FALSE : TRUE; + } + } + } + + return $this->isNegated() ? TRUE : FALSE; + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (!empty($this->configuration['negate'])) { + return $this->t('The node does not have a value in its PID field with the namespace @namespace.', ['@namespace' => $this->configuration['namespace']]); + } + else { + return $this->t('The node has a value in its PID field with the namespace @namespace.', ['@namespace' => $this->configuration['namespace']]); + } + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array_merge( + ['namespace' => '', 'pid_field' => 'field_pid'], + parent::defaultConfiguration() + ); + } + +} diff --git a/src/Plugin/Condition/NodeHasParent.php b/src/Plugin/Condition/NodeHasParent.php new file mode 100644 index 00000000..df95c6ad --- /dev/null +++ b/src/Plugin/Condition/NodeHasParent.php @@ -0,0 +1,168 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return parent::defaultConfiguration() + [ + 'parent_reference_field' => 'field_member_of', + 'parent_nid' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['parent_nid'] = [ + '#type' => 'entity_autocomplete', + '#title' => t('Parent node'), + '#default_value' => $this->entityTypeManager->getStorage('node')->load($this->configuration['parent_nid']), + '#required' => TRUE, + '#description' => t("Can be a collection node or a compound object."), + '#target_type' => 'node', + ]; + $field_map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference'); + $node_fields = array_keys($field_map['node']); + $options = array_combine($node_fields, $node_fields); + $form['parent_reference_field'] = [ + '#type' => 'select', + '#title' => t('Field that contains reference to parents'), + '#options' => $options, + '#default_value' => $this->configuration['parent_reference_field'], + '#required' => TRUE, + '#description' => t("Machine name of field that contains references to parent node."), + ]; + + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['parent_nid'] = $form_state->getValue('parent_nid'); + $this->configuration['parent_reference_field'] = $form_state->getValue('parent_reference_field'); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + if (empty($this->configuration['parent_nid']) && !$this->isNegated()) { + return TRUE; + } + + $node = $this->getContextValue('node'); + if (!$node) { + return FALSE; + } + return $this->evaluateEntity($node); + } + + /** + * Evaluates if an entity has the specified node as its parent. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to evalute. + * + * @return bool + * TRUE if entity references the specified parent. + */ + protected function evaluateEntity(EntityInterface $entity) { + foreach ($entity->referencedEntities() as $referenced_entity) { + if ($entity->getEntityTypeID() == 'node' && $referenced_entity->getEntityTypeId() == 'node') { + $parent_reference_field = $this->configuration['parent_reference_field']; + $field = $entity->get($parent_reference_field); + if (!$field->isEmpty()) { + $nids = $field->getValue(); + foreach ($nids as $nid) { + if ($nid['target_id'] == $this->configuration['parent_nid']) { + return $this->isNegated() ? FALSE : TRUE; + } + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (!empty($this->configuration['negate'])) { + return $this->t('The node does not have node @nid as its parent.', ['@nid' => $this->configuration['parent_nid']]); + } + else { + return $this->t('The node has node @nid as its parent.', ['@nid' => $this->configuration['parent_nid']]); + } + } + +} diff --git a/src/Plugin/Condition/NodeHasTerm.php b/src/Plugin/Condition/NodeHasTerm.php index 774f6d7e..dae52ad0 100644 --- a/src/Plugin/Condition/NodeHasTerm.php +++ b/src/Plugin/Condition/NodeHasTerm.php @@ -83,11 +83,12 @@ class NodeHasTerm extends ConditionPluginBase implements ContainerFactoryPluginI * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $default = []; if (isset($this->configuration['uri']) && !empty($this->configuration['uri'])) { - $default = $this->utils->getTermForUri($this->configuration['uri']); - } - else { - $default = NULL; + $uris = explode(',', $this->configuration['uri']); + foreach ($uris as $uri) { + $default[] = $this->utils->getTermForUri($uri); + } } $form['term'] = [ @@ -96,6 +97,7 @@ class NodeHasTerm extends ConditionPluginBase implements ContainerFactoryPluginI '#tags' => TRUE, '#default_value' => $default, '#target_type' => 'taxonomy_term', + '#required' => TRUE, ]; return parent::buildConfigurationForm($form, $form_state); @@ -108,12 +110,18 @@ class NodeHasTerm extends ConditionPluginBase implements ContainerFactoryPluginI // Set URI for term if possible. $this->configuration['uri'] = NULL; $value = $form_state->getValue('term'); + $uris = []; if (!empty($value)) { - $tid = $value[0]['target_id']; - $term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid); - $uri = $this->utils->getUriForTerm($term); - if ($uri) { - $this->configuration['uri'] = $uri; + foreach ($value as $target) { + $tid = $target['target_id']; + $term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid); + $uri = $this->utils->getUriForTerm($term); + if ($uri) { + $uris[] = $uri; + } + } + if (!empty($uris)) { + $this->configuration['uri'] = implode(',', $uris); } } parent::submitConfigurationForm($form, $form_state); @@ -144,18 +152,35 @@ class NodeHasTerm extends ConditionPluginBase implements ContainerFactoryPluginI * TRUE if entity has all the specified term(s), otherwise FALSE. */ protected function evaluateEntity(EntityInterface $entity) { - foreach ($entity->referencedEntities() as $referenced_entity) { - if ($referenced_entity->getEntityTypeId() == 'taxonomy_term' && $referenced_entity->hasField(IslandoraUtils::EXTERNAL_URI_FIELD)) { - $field = $referenced_entity->get(IslandoraUtils::EXTERNAL_URI_FIELD); - if (!$field->isEmpty()) { - $link = $field->first()->getValue(); - if ($link['uri'] == $this->configuration['uri']) { - return $this->isNegated() ? FALSE : TRUE; - } - } - } + // Find the terms on the node. + $terms = array_filter($entity->referencedEntities(), function ($entity) { + return $entity->getEntityTypeId() == 'taxonomy_term' && + $entity->hasField(IslandoraUtils::EXTERNAL_URI_FIELD) && + !$entity->get(IslandoraUtils::EXTERNAL_URI_FIELD)->isEmpty(); + }); + + // Get their URIs. + $haystack = array_map(function ($term) { + return $term->get(IslandoraUtils::EXTERNAL_URI_FIELD)->first()->getValue()['uri']; + }, + $terms + ); + + // FALSE if there's no URIs on the node. + if (empty($haystack)) { + return $this->isNegated() ? TRUE : FALSE; + } + + // Get the URIs to look for. It's a required field, so there + // will always be one. + $needles = explode(',', $this->configuration['uri']); + + // TRUE if every needle is in the haystack. + if (count(array_intersect($needles, $haystack)) == count($needles)) { + return $this->isNegated() ? FALSE : TRUE; } + // Otherwise, FALSE. return $this->isNegated() ? TRUE : FALSE; } diff --git a/src/Plugin/Condition/NodeIsPublished.php b/src/Plugin/Condition/NodeIsPublished.php new file mode 100644 index 00000000..a378aaa4 --- /dev/null +++ b/src/Plugin/Condition/NodeIsPublished.php @@ -0,0 +1,94 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + $node = $this->getContextValue('node'); + if (!$node && !$this->isNegated()) { + return FALSE; + } + if ($node->isPublished() && !$this->isNegated()) { + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (!empty($this->configuration['negate'])) { + return $this->t('The node is not published.'); + } + else { + return $this->t('The node is published.'); + } + } + +} diff --git a/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php new file mode 100644 index 00000000..56c68cd5 --- /dev/null +++ b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php @@ -0,0 +1,102 @@ +t('Alter JSON-LD Type context reaction.'); + } + + /** + * {@inheritdoc} + */ + public function execute(EntityInterface $entity = NULL, array &$normalized = NULL, array $context = NULL) { + // Check that the source field exists and there's some RDF + // to manipulate. + $config = $this->getConfiguration(); + $ok = $entity->hasField($config['source_field']) && + !empty($entity->get($config['source_field'])->getValue()) && + isset($normalized['@graph']) && + is_array($normalized['@graph']) && + !empty($normalized['@graph']); + + if (!$ok) { + return; + } + + // Search for the entity in the graph. + foreach ($normalized['@graph'] as &$elem) { + if ($elem['@id'] === $this->getSubjectUrl($entity)) { + foreach ($entity->get($config['source_field'])->getValue() as $type) { + // If the configured field is using an entity reference, + // we will see if it uses the core config's field_external_uri. + if (array_key_exists('target_id', $type)) { + $target_type = $entity->get($config['source_field'])->getFieldDefinition()->getSetting('target_type'); + $referenced_entity = \Drupal::entityTypeManager()->getStorage($target_type)->load($type['target_id']); + if ($referenced_entity->hasField('field_external_uri') && + !empty($referenced_entity->get('field_external_uri')->getValue())) { + foreach ($referenced_entity->get('field_external_uri')->getValue() as $value) { + $elem['@type'][] = $value['uri']; + } + } + } + else { + $elem['@type'][] = NormalizerBase::escapePrefix($type['value'], $context['namespaces']); + } + } + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $options = []; + $fieldsArray = \Drupal::service('entity_field.manager')->getFieldMap(); + foreach ($fieldsArray as $entity_type => $entity_fields) { + foreach ($entity_fields as $field => $field_properties) { + $options[$field] = $this->t('@field (@bundles)', [ + '@field' => $field, + '@bundles' => implode(', ', array_keys($field_properties['bundles'])), + ]); + } + } + + $config = $this->getConfiguration(); + $form['source_field'] = [ + '#type' => 'select', + '#title' => $this->t('Source Field'), + '#options' => $options, + '#description' => $this->t("Select the field containing the type predicates."), + '#default_value' => isset($config['source_field']) ? $config['source_field'] : '', + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->setConfiguration(['source_field' => $form_state->getValue('source_field')]); + } + +} diff --git a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php index e393f7e6..05f2f8c0 100644 --- a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php +++ b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php @@ -2,13 +2,14 @@ namespace Drupal\islandora\Plugin\ContextReaction; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\islandora\ContextReaction\NormalizerAlterReaction; use Drupal\islandora\MediaSource\MediaSourceService; use Drupal\jsonld\Normalizer\NormalizerBase; use Drupal\media\MediaInterface; +use Drupal\islandora\IslandoraUtils; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -19,17 +20,34 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * label = @Translation("Map URI to predicate") * ) */ -class MappingUriPredicateReaction extends NormalizerAlterReaction implements ContainerFactoryPluginInterface { +class MappingUriPredicateReaction extends NormalizerAlterReaction { const URI_PREDICATE = 'drupal_uri_predicate'; + /** + * Media source service. + * + * @var \Drupal\islandora\MediaSource\MediaSourceService + */ protected $mediaSource; /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MediaSourceService $media_source) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + public function __construct(array $configuration, + $plugin_id, + $plugin_definition, + ConfigFactoryInterface $config_factory, + IslandoraUtils $utils, + MediaSourceService $media_source) { + + parent::__construct( + $configuration, + $plugin_id, + $plugin_definition, + $config_factory, + $utils + ); $this->mediaSource = $media_source; } @@ -41,6 +59,8 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction implements Con $configuration, $plugin_id, $plugin_definition, + $container->get('config.factory'), + $container->get('islandora.utils'), $container->get('islandora.media_source_service') ); } @@ -59,19 +79,17 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction implements Con $config = $this->getConfiguration(); $drupal_predicate = $config[self::URI_PREDICATE]; if (!is_null($drupal_predicate) && !empty($drupal_predicate)) { - $url = $entity - ->toUrl('canonical', ['absolute' => TRUE]) - ->setRouteParameter('_format', 'jsonld') - ->toString(); + $url = $this->getSubjectUrl($entity); if ($context['needs_jsonldcontext'] === FALSE) { $drupal_predicate = NormalizerBase::escapePrefix($drupal_predicate, $context['namespaces']); } if (isset($normalized['@graph']) && is_array($normalized['@graph'])) { foreach ($normalized['@graph'] as &$graph) { if (isset($graph['@id']) && $graph['@id'] == $url) { + // Swap media and file urls. if ($entity instanceof MediaInterface) { $file = $this->mediaSource->getSourceFile($entity); - $url = $file->url('canonical', ['absolute' => TRUE]); + $graph['@id'] = $this->utils->getDownloadUrl($file); } if (isset($graph[$drupal_predicate])) { if (!is_array($graph[$drupal_predicate])) { @@ -82,7 +100,7 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction implements Con $tmp = $graph[$drupal_predicate]; $graph[$drupal_predicate] = [$tmp]; } - elseif (array_search($url, array_column($graph[$drupal_predicate], '@value'))) { + elseif (array_search($url, array_column($graph[$drupal_predicate], '@id'))) { // Don't add it if it already exists. return; } @@ -90,7 +108,7 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction implements Con else { $graph[$drupal_predicate] = []; } - $graph[$drupal_predicate][] = ["@value" => $url]; + $graph[$drupal_predicate][] = ["@id" => $url]; return; } } diff --git a/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php b/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php new file mode 100644 index 00000000..321dd05d --- /dev/null +++ b/src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php @@ -0,0 +1,136 @@ +utils = $utils; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('current_user'), + $container->get('entity.manager')->getStorage('image_style'), + $container->get('islandora.utils') + ); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = parent::viewElements($items, $langcode); + + $image_link_setting = $this->getSetting('image_link'); + // Check if the formatter involves a link. + if ($image_link_setting != 'content') { + return $elements; + } + + $entity = $items->getEntity(); + if ($entity->isNew() || $entity->getEntityTypeId() != 'media') { + return $elements; + } + + $node = $this->utils->getParentNode($entity); + + if ($node === NULL) { + return $elements; + } + + $url = $node->urlInfo(); + + foreach ($elements as &$element) { + $element['#url'] = $url; + } + + return $elements; + } + +} diff --git a/src/Plugin/views/field/IntegerWeightSelector.php b/src/Plugin/views/field/IntegerWeightSelector.php new file mode 100644 index 00000000..731ae4b3 --- /dev/null +++ b/src/Plugin/views/field/IntegerWeightSelector.php @@ -0,0 +1,101 @@ +options['id'] . '--' . $this->view->row_index . '-->'); + } + + /** + * {@inheritdoc} + */ + public function viewsForm(array &$form, FormStateInterface $form_state) { + // The view is empty, abort. + if (empty($this->view->result)) { + return; + } + + $form[$this->options['id']] = [ + '#tree' => TRUE, + ]; + + // Use the existing values of this result set to populate the options. + $options = []; + foreach ($this->view->result as $row_index => $row) { + $options[$this->getValue($row)] = $this->getValue($row); + } + + // Now that we have all the available weight values, populate the forms. + foreach ($this->view->result as $row_index => $row) { + $entity = $row->_entity; + $field_langcode = $entity->getEntityTypeId() . '__' . $this->field . '_langcode'; + + $form[$this->options['id']][$row_index]['weight'] = [ + '#type' => 'select', + '#options' => $options, + '#default_value' => $this->getValue($row), + '#attributes' => ['class' => ['weight-selector']], + ]; + + $form[$this->options['id']][$row_index]['entity'] = [ + '#type' => 'value', + '#value' => $entity, + ]; + + $form[$this->options['id']][$row_index]['langcode'] = [ + '#type' => 'value', + '#value' => $row->{$field_langcode}, + ]; + } + + $form['views_field'] = [ + '#type' => 'value', + '#value' => $this->field, + ]; + + $form['#action'] = \Drupal::request()->getRequestUri(); + } + + /** + * {@inheritdoc} + */ + public function viewsFormSubmit(array &$form, FormStateInterface $form_state) { + $field_name = $form_state->getValue('views_field'); + $rows = $form_state->getValue($field_name); + + foreach ($rows as $row) { + if ($row['langcode']) { + $entity = $row['entity']->getTranslation($row['langcode']); + } + else { + $entity = $row['entity']; + } + if ($entity && $entity->hasField($field_name)) { + $entity->set($field_name, $row['weight']); + $entity->save(); + } + } + } + +} diff --git a/src/PresetReaction/PresetReaction.php b/src/PresetReaction/PresetReaction.php index 0584b997..98aa6946 100644 --- a/src/PresetReaction/PresetReaction.php +++ b/src/PresetReaction/PresetReaction.php @@ -14,6 +14,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class PresetReaction extends ContextReactionPluginBase implements ContainerFactoryPluginInterface { + /** + * Action storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ protected $actionStorage; /** diff --git a/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml b/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml new file mode 100644 index 00000000..78c510fc --- /dev/null +++ b/tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml @@ -0,0 +1,9 @@ +name: 'Integer weight test views' +type: module +description: 'Provides default views for integer weight views tests.' +package: Testing +core: 8.x +dependencies: + - drupal:node + - drupal:views + - drupal:language diff --git a/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml b/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml new file mode 100644 index 00000000..c08b8400 --- /dev/null +++ b/tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml @@ -0,0 +1,251 @@ +langcode: en +status: true +dependencies: + config: + - node.type.repo_item + module: + - node + - user +id: test_integer_weight +label: 'test integer weight' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + 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 + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: 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: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + 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 + field_integer_weight: + id: field_integer_weight + table: node__field_integer_weight + field: field_integer_weight + relationship: none + group_type: group + admin_label: '' + label: 'Integer weight selector (field_integer_weight)' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + 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: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + range: '20' + plugin_id: integer_weight_selector + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + repo_item:repo_item + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + field_integer_weight_value: + id: field_integer_weight_value + table: node__field_integer_weight + field: field_integer_weight_value + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + plugin_id: standard + title: 'test weight' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test-integer-weight + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + diff --git a/tests/src/Functional/DeleteMediaTest.php b/tests/src/Functional/DeleteMediaTest.php index 1a3673aa..03192194 100644 --- a/tests/src/Functional/DeleteMediaTest.php +++ b/tests/src/Functional/DeleteMediaTest.php @@ -35,31 +35,6 @@ class DeleteMediaTest extends IslandoraFunctionalTestBase { list($this->file, $this->media) = $this->makeMediaAndFile($account); } - /** - * Tests the delete_media action. - * - * @covers \Drupal\islandora\Plugin\Action\DeleteMedia::execute - */ - public function testDeleteMedia() { - $action = $this->container->get('entity_type.manager')->getStorage('action')->load('delete_media'); - - $mid = $this->media->id(); - $fid = $this->file->id(); - - $action->execute([$this->media]); - - // Attempt to reload the entities. - // Media should be gone but file should remain. - $this->assertTrue( - !$this->container->get('entity_type.manager')->getStorage('media')->load($mid), - "Media must be deleted after running action" - ); - $this->assertTrue( - $this->container->get('entity_type.manager')->getStorage('file')->load($fid), - "File must remain after running action" - ); - } - /** * Tests the delete_media_and_file action. * diff --git a/tests/src/Functional/EntityBundleTest.php b/tests/src/Functional/EntityBundleTest.php new file mode 100644 index 00000000..1ffc261e --- /dev/null +++ b/tests/src/Functional/EntityBundleTest.php @@ -0,0 +1,43 @@ +drupalCreateUser([ + 'bypass node access', + 'administer contexts', + 'administer taxonomy', + ]); + $this->drupalLogin($account); + + $this->createContext('Test', 'test'); + $this->addCondition('test', 'entity_bundle'); + $this->getSession()->getPage()->checkField("edit-conditions-entity-bundle-bundles-test-type"); + $this->getSession()->getPage()->findById("edit-conditions-entity-bundle-context-mapping-node")->selectOption("@node.node_route_context:node"); + $this->getSession()->getPage()->pressButton(t('Save and continue')); + $this->addPresetReaction('test', 'index', 'hello_world'); + + // Create a new test_type confirm Hello World! is printed to the screen. + $this->postNodeAddForm('test_type', ['title[0][value]' => 'Test Node'], 'Save'); + $this->assertSession()->pageTextContains("Hello World!"); + + // Create a new term and confirm Hellow World! is NOT printed to the screen. + $this->postTermAddForm('test_vocabulary', ['name[0][value]' => 'Test Term'], 'Save'); + $this->assertSession()->pageTextNotContains("Hello World!"); + + } + +} diff --git a/tests/src/Functional/FormDisplayAlterReactionTest.php b/tests/src/Functional/FormDisplayAlterReactionTest.php index d5d41dea..08501579 100644 --- a/tests/src/Functional/FormDisplayAlterReactionTest.php +++ b/tests/src/Functional/FormDisplayAlterReactionTest.php @@ -40,7 +40,7 @@ class FormDisplayAlterReactionTest extends IslandoraFunctionalTestBase { $url = $this->getUrl(); // Visit the edit url and make sure we're on the default form display - // (e.g. there's an autocomplete for the member of field); + // (e.g. there's an autocomplete for the member of field). $this->drupalGet($url . "/edit"); $this->assertSession()->pageTextContains("Member Of"); @@ -62,4 +62,3 @@ class FormDisplayAlterReactionTest extends IslandoraFunctionalTestBase { } } - diff --git a/tests/src/Functional/GenerateDerivativeTestBase.php b/tests/src/Functional/GenerateDerivativeTestBase.php new file mode 100644 index 00000000..c74ff255 --- /dev/null +++ b/tests/src/Functional/GenerateDerivativeTestBase.php @@ -0,0 +1,119 @@ +createUserAndLogin(); + $this->createImageTag(); + $this->createPreservationMasterTag(); + + // 'Service File' tag. + $this->serviceFileTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ + 'name' => 'Service File', + 'vid' => $this->testVocabulary->id(), + 'field_external_uri' => [['uri' => "http://pcdm.org/use#ServiceFile"]], + ]); + $this->serviceFileTerm->save(); + + // Node to be referenced via media_of. + $this->node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => $this->testType->id(), + 'title' => 'Test Node', + 'field_tags' => [$this->imageTerm->id()], + ]); + $this->node->save(); + } + + /** + * Asserts a derivative event was delivered. + * + * @param array $expected + * The expected values. + */ + protected function checkMessage(array $expected) { + // Verify message is sent. + $stomp = $this->container->get('islandora.stomp'); + try { + $stomp->subscribe('generate-test-derivative'); + while ($msg = $stomp->read()) { + $headers = $msg->getHeaders(); + $this->assertTrue( + isset($headers['Authorization']), + "Authorization header must be set" + ); + $matches = []; + $this->assertTrue( + preg_match('/^Bearer (.*)/', $headers['Authorization'], $matches), + "Authorization header must be a bearer token" + ); + $this->assertTrue( + count($matches) == 2 && !empty($matches[1]), + "Bearer token must not be empty" + ); + + $body = $msg->getBody(); + $body = json_decode($body, TRUE); + + $type = $body['type']; + $this->assertTrue($type == 'Activity', "Expected 'Activity', received $type"); + + $summary = $body['summary']; + $this->assertTrue($summary == 'Generate Derivative', "Expected 'Generate Derivative', received $summary"); + + $content = $body['attachment']['content']; + $this->assertTrue( + strpos($content['source_uri'], $expected['source_uri']) !== FALSE, + "Expected source uri should contain the file." + ); + + $this->assertTrue( + strpos($content['destination_uri'], $expected['destination_uri']) !== FALSE, + "Expected destination uri should reference both node and term" + ); + $this->assertEquals($expected['file_upload_uri'], + $content['file_upload_uri'], + "Expected file upload uri should contain the scheme and path of the derivative" + ); + + $this->assertEquals($expected['mimetype'], $content['mimetype'], "Expected mimetype '{$expected['mimetype']}', received {$content['mimetype']}"); + + $this->assertEquals($expected['args'], $content['args'], "Expected bundle '{$expected['args']}', received {$content['args']}"); + + } + $stomp->unsubscribe(); + } + catch (StompException $e) { + $this->assertTrue(FALSE, "There was an error connecting to the stomp broker"); + } + } + +} diff --git a/tests/src/Functional/IslandoraFunctionalTestBase.php b/tests/src/Functional/IslandoraFunctionalTestBase.php index b9f99dd0..9a50fb05 100644 --- a/tests/src/Functional/IslandoraFunctionalTestBase.php +++ b/tests/src/Functional/IslandoraFunctionalTestBase.php @@ -5,11 +5,11 @@ namespace Drupal\Tests\islandora\Functional; use Drupal\Core\Config\FileStorage; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; use Drupal\link\LinkItemInterface; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field\Traits\EntityReferenceTestTrait; +use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\TestFileCreationTrait; -use Drupal\Tests\media\Functional\MediaFunctionalTestCreateMediaTypeTrait; /** * Base class for Functional tests. @@ -18,10 +18,16 @@ class IslandoraFunctionalTestBase extends BrowserTestBase { use EntityReferenceTestTrait; use TestFileCreationTrait; - use MediaFunctionalTestCreateMediaTypeTrait; + use MediaTypeCreationTrait; - protected static $modules = ['context_ui', 'islandora']; + /** + * {@inheritdoc} + */ + protected static $modules = ['context_ui', 'field_ui', 'islandora']; + /** + * {@inheritdoc} + */ protected static $configSchemaCheckerExclusions = [ 'jwt.config', 'context.context.test', @@ -29,14 +35,44 @@ class IslandoraFunctionalTestBase extends BrowserTestBase { 'context.context.media', 'context.context.file', 'key.key.test', + 'media.settings', ]; + /** + * Test node type. + * + * @var \Drupal\node\Entity\NodeType + */ protected $testType; + /** + * Test media type. + * + * @var \Drupal\media\Entity\MediaType + */ protected $testMediaType; + /** + * Test vocabulary. + * + * @var \Drupal\taxonomy\Entity\Vocabulary + */ protected $testVocabulary; + /** + * Term to belong to the node. + * + * @var \Drupal\taxonomy\TermInterface + */ + protected $imageTerm; + + /** + * Term to belong to the source media. + * + * @var \Drupal\taxonomy\TermInterface + */ + protected $preservationMasterTerm; + /** * {@inheritdoc} */ @@ -143,7 +179,7 @@ EOD; $this->createEntityReferenceField('node', 'test_type', 'field_tags', 'Tags', 'taxonomy_term', 'default', [], 2); // Create a media type. - $this->testMediaType = $this->createMediaType(['bundle' => 'test_media_type'], 'file'); + $this->testMediaType = $this->createMediaType('file', ['id' => 'test_media_type']); $this->testMediaType->save(); $this->createEntityReferenceField('media', $this->testMediaType->id(), 'field_media_of', 'Media Of', 'node', 'default', [], 2); $this->createEntityReferenceField('media', $this->testMediaType->id(), 'field_tags', 'Tags', 'taxonomy_term', 'default', [], 2); @@ -156,11 +192,55 @@ EOD; $destination->write($name, $source->read($name)); } + $media_settings = $this->container->get('config.factory')->getEditable('media.settings'); + $media_settings->set('standalone_url', TRUE); + $media_settings->save(TRUE); + // Cache clear / rebuild. drupal_flush_all_caches(); $this->container->get('router.builder')->rebuild(); } + /** + * Create a new user and log them in. + */ + protected function createUserAndLogin() { + // Create a test user. + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + return $account; + } + + /** + * Create an Image tag. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function createImageTag() { + // 'Image' tag. + $this->imageTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ + 'name' => 'Image', + 'vid' => $this->testVocabulary->id(), + 'field_external_uri' => [['uri' => "http://purl.org/coar/resource_type/c_c513"]], + ]); + $this->imageTerm->save(); + } + + /** + * Create a Preservation Master tag. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function createPreservationMasterTag() { + // 'Preservation Master' tag. + $this->preservationMasterTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ + 'name' => 'Preservation Master', + 'vid' => $this->testVocabulary->id(), + 'field_external_uri' => [['uri' => "http://pcdm.org/use#PreservationMasterFile"]], + ]); + $this->preservationMasterTerm->save(); + } + /** * Creates a test context. */ @@ -196,6 +276,14 @@ EOD; $this->assertSession()->statusCodeEquals(200); } + /** + * Create a new node by posting its add form. + */ + protected function postTermAddForm($taxomony_id, $values, $button_text) { + $this->drupalPostForm("admin/structure/taxonomy/manage/$taxomony_id/add", $values, t('@text', ['@text' => $button_text])); + $this->assertSession()->statusCodeEquals(200); + } + /** * Edits a node by posting its edit form. */ diff --git a/tests/src/Functional/IslandoraImageFormatterTest.php b/tests/src/Functional/IslandoraImageFormatterTest.php new file mode 100644 index 00000000..aa85162c --- /dev/null +++ b/tests/src/Functional/IslandoraImageFormatterTest.php @@ -0,0 +1,93 @@ +drupalCreateUser([ + 'bypass node access', + 'view media', + 'create media', + ]); + $this->drupalLogin($account); + + // Create an image media type. + $testImageMediaType = $this->createMediaType('image', ['id' => 'test_image_media_type']); + $testImageMediaType->save(); + $this->createEntityReferenceField('media', $testImageMediaType->id(), 'field_media_of', 'Media Of', 'node', 'default', [], 2); + + // Set the display mode to use the islandora_image formatter. + // Also, only show the image on display to remove clutter. + $display_options = [ + 'type' => 'islandora_image', + 'settings' => ['image_style' => NULL, 'image_link' => 'content'], + ]; + $display = entity_get_display('media', $testImageMediaType->id(), 'default'); + $display->setComponent('field_media_image', $display_options) + ->removeComponent('created') + ->removeComponent('uid') + ->removeComponent('thumbnail') + ->save(); + + // Make a node. + $node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => $this->testType->id(), + 'title' => 'Test Node', + ]); + $node->save(); + + // Make a image for the Media. + $file = $this->container->get('entity_type.manager')->getStorage('file')->create([ + 'uid' => $account->id(), + 'uri' => "public://test.jpeg", + 'filename' => "test.jpeg", + 'filemime' => "image/jpeg", + 'status' => FILE_STATUS_PERMANENT, + ]); + $file->save(); + + // Make the media, and associate it with the image and node. + $media = $this->container->get('entity_type.manager')->getStorage('media')->create([ + 'bundle' => $testImageMediaType->id(), + 'name' => 'Media', + 'field_media_image' => + [ + 'target_id' => $file->id(), + 'alt' => 'Some Alt', + 'title' => 'Some Title', + ], + 'field_media_of' => ['target_id' => $node->id()], + ]); + $media->save(); + + // View the media. + $this->drupalGet("media/1"); + + // Assert that the image is rendered into html as a link pointing + // to the Node, not the Media (that's what the islandora_image + // formatter does). + $elements = $this->xpath( + '//a[@href=:path]/img[@src=:url and @alt=:alt and @title=:title]', + [ + ':path' => $node->url(), + ':url' => file_url_transform_relative(file_create_url($file->getFileUri())), + ':alt' => 'Some Alt', + ':title' => 'Some Title', + ] + ); + $this->assertEqual(count($elements), 1, 'Image linked to content formatter displaying points to Node and not Media.'); + } + +} diff --git a/tests/src/Functional/IslandoraSettingsFormTest.php b/tests/src/Functional/IslandoraSettingsFormTest.php new file mode 100644 index 00000000..f33a10a9 --- /dev/null +++ b/tests/src/Functional/IslandoraSettingsFormTest.php @@ -0,0 +1,61 @@ +drupalCreateUser([ + 'bypass node access', + 'administer site configuration', + 'view media', + 'create media', + 'update media', + ]); + $this->drupalLogin($account); + } + + /** + * Test Gemini URL validation. + */ + public function testGeminiUri() { + $this->drupalGet('/admin/config/islandora/core'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains("Gemini URL"); + $this->assertSession()->fieldValueEquals('edit-gemini-url', ''); + + $this->drupalPostForm('admin/config/islandora/core', ['edit-gemini-url' => 'not_a_url'], t('Save configuration')); + $this->assertSession()->pageTextContainsOnce("Cannot parse URL not_a_url"); + + $this->drupalPostForm('admin/config/islandora/core', ['edit-gemini-url' => 'http://whaturl.bob'], t('Save configuration')); + $this->assertSession()->pageTextContainsOnce("Cannot connect to URL http://whaturl.bob"); + } + + /** + * Test block on choosing Pseudo field bundles without a Gemini URL. + */ + public function testPseudoFieldBundles() { + $this->drupalGet('/admin/config/islandora/core'); + $this->assertSession()->statusCodeEquals(200); + + $this->drupalPostForm('admin/config/islandora/core', [ + 'gemini_pseudo_bundles[test_type:node]' => TRUE, + ], t('Save configuration')); + $this->assertSession()->pageTextContainsOnce("Must enter Gemini URL before selecting bundles to display a pseudo field on."); + + } + +} diff --git a/tests/src/Functional/JsonldTypeAlterReactionTest.php b/tests/src/Functional/JsonldTypeAlterReactionTest.php new file mode 100644 index 00000000..fdf12057 --- /dev/null +++ b/tests/src/Functional/JsonldTypeAlterReactionTest.php @@ -0,0 +1,87 @@ +drupalCreateUser([ + 'bypass node access', + 'administer contexts', + 'administer node fields', + ]); + $this->drupalLogin($account); + + // Add the typed predicate we will select in the reaction config. + // Taken from FieldUiTestTrait->fieldUIAddNewField. + $this->drupalPostForm('admin/structure/types/manage/test_type/fields/add-field', [ + 'new_storage_type' => 'string', + 'label' => 'Typed Predicate', + 'field_name' => 'type_predicate', + ], t('Save and continue')); + $this->drupalPostForm(NULL, [], t('Save field settings')); + $this->drupalPostForm(NULL, [], t('Save settings')); + $this->assertRaw('field_type_predicate', 'Redirected to "Manage fields" page.'); + + // Add the test node. + $this->postNodeAddForm('test_type', [ + 'title[0][value]' => 'Test Node', + 'field_type_predicate[0][value]' => 'schema:Organization', + ], t('Save')); + $this->assertSession()->pageTextContains("Test Node"); + $url = $this->getUrl(); + + // Make sure the node exists. + $this->drupalGet($url); + $this->assertSession()->statusCodeEquals(200); + + $contents = $this->drupalGet($url . '?_format=jsonld'); + $this->assertSession()->statusCodeEquals(200); + $json = \GuzzleHttp\json_decode($contents, TRUE); + $this->assertArrayHasKey('@type', + $json['@graph'][0], 'Missing @type'); + $this->assertEquals( + 'http://schema.org/Thing', + $json['@graph'][0]['@type'][0], + 'Missing @type value of http://schema.org/Thing' + ); + + // Add the test context. + $context_name = 'test'; + $reaction_id = 'alter_jsonld_type'; + + $this->createContext('Test', $context_name); + $this->drupalGet("admin/structure/context/$context_name/reaction/add/$reaction_id"); + $this->assertSession()->statusCodeEquals(200); + + $this->drupalGet("admin/structure/context/$context_name"); + $this->getSession()->getPage() + ->fillField("Source Field", "field_type_predicate"); + $this->getSession()->getPage()->pressButton("Save and continue"); + $this->assertSession() + ->pageTextContains("The context $context_name has been saved"); + + $this->addCondition('test', 'entity_bundle'); + $this->getSession()->getPage()->checkField("edit-conditions-entity-bundle-bundles-test-type"); + $this->getSession()->getPage()->findById("edit-conditions-entity-bundle-context-mapping-node")->selectOption("@node.node_route_context:node"); + $this->getSession()->getPage()->pressButton(t('Save and continue')); + + // Check for the new @type from the field_type_predicate value. + $new_contents = $this->drupalGet($url . '?_format=jsonld'); + $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $this->assertTrue( + in_array('http://schema.org/Organization', $json['@graph'][0]['@type']), + 'Missing altered @type value of http://schema.org/Organization' + ); + } + +} diff --git a/tests/src/Functional/LinkHeaderTest.php b/tests/src/Functional/LinkHeaderTest.php index 9f7f8328..472986c2 100644 --- a/tests/src/Functional/LinkHeaderTest.php +++ b/tests/src/Functional/LinkHeaderTest.php @@ -39,45 +39,15 @@ class LinkHeaderTest extends IslandoraFunctionalTestBase { */ protected $file; - /** - * Term to belong to the referencer. - * - * @var \Drupal\taxonomy\TermInterface - */ - protected $imageTerm; - - /** - * Term to belong to the media. - * - * @var \Drupal\taxonomy\TermInterface - */ - protected $preservationMasterTerm; - /** * {@inheritdoc} */ public function setUp() { parent::setUp(); - // Create a test user. - $account = $this->drupalCreateUser(); - $this->drupalLogin($account); - - // 'Image' tag. - $this->imageTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ - 'name' => 'Image', - 'vid' => $this->testVocabulary->id(), - 'field_external_uri' => [['uri' => "http://purl.org/coar/resource_type/c_c513"]], - ]); - $this->imageTerm->save(); - - // 'Preservation Master' tag. - $this->preservationMasterTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ - 'name' => 'Preservation Master', - 'vid' => $this->testVocabulary->id(), - 'field_external_uri' => [['uri' => "http://pcdm.org/use#PreservationMasterFile"]], - ]); - $this->preservationMasterTerm->save(); + $account = $this->createUserAndLogin(); + $this->createImageTag(); + $this->createPreservationMasterTag(); // Node to be referenced via member of. $this->referenced = $this->container->get('entity_type.manager')->getStorage('node')->create([ diff --git a/tests/src/Functional/MappingUriPredicateReactionTest.php b/tests/src/Functional/MappingUriPredicateReactionTest.php index 31d27bd5..bf7bff6e 100644 --- a/tests/src/Functional/MappingUriPredicateReactionTest.php +++ b/tests/src/Functional/MappingUriPredicateReactionTest.php @@ -107,7 +107,7 @@ class MappingUriPredicateReactionTest extends IslandoraFunctionalTestBase { ); $this->assertEquals( "$url?_format=jsonld", - $json['@graph'][0]['http://www.w3.org/2002/07/owl#sameAs'][0]['@value'], + $json['@graph'][0]['http://www.w3.org/2002/07/owl#sameAs'][0]['@id'], 'Missing alter added predicate.' ); @@ -129,9 +129,67 @@ class MappingUriPredicateReactionTest extends IslandoraFunctionalTestBase { $json['@graph'][0], 'Still has old predicate'); $this->assertEquals( "$url?_format=jsonld", - $json['@graph'][0]['http://example.org/first/second'][0]['@value'], + $json['@graph'][0]['http://example.org/first/second'][0]['@id'], 'Missing alter added predicate.' ); } + /** + * @covers \Drupal\islandora\Plugin\ContextReaction\MappingUriPredicateReaction + */ + public function testMappingReactionForMedia() { + $account = $this->drupalCreateUser([ + 'create media', + 'view media', + 'administer contexts', + ]); + $this->drupalLogin($account); + + $context_name = 'test'; + $reaction_id = 'islandora_map_uri_predicate'; + + list($file, $media) = $this->makeMediaAndFile($account); + $media_url = $media->url('canonical', ['absolute' => TRUE]); + $file_url = $file->url('canonical', ['absolute' => TRUE]); + + $this->drupalGet($media_url); + $this->assertSession()->statusCodeEquals(200); + + $contents = $this->drupalGet($media_url . '?_format=jsonld'); + $this->assertSession()->statusCodeEquals(200); + $json = \GuzzleHttp\json_decode($contents, TRUE); + $this->assertEquals( + "$media_url?_format=jsonld", + $json['@graph'][0]['@id'], + 'Swapped file and media urls when not configured' + ); + $this->assertArrayNotHasKey('http://www.iana.org/assignments/relation/describedby', + $json['@graph'][0], 'Has predicate when not configured'); + + $this->createContext('Test', $context_name); + $this->drupalGet("admin/structure/context/$context_name/reaction/add/$reaction_id"); + $this->assertSession()->statusCodeEquals(200); + + // Use an existing prefix. + $this->getSession()->getPage() + ->fillField("Drupal URI predicate", "iana:describedby"); + $this->getSession()->getPage()->pressButton("Save and continue"); + $this->assertSession() + ->pageTextContains("The context $context_name has been saved"); + + $new_contents = $this->drupalGet($media_url . '?_format=jsonld'); + $json = \GuzzleHttp\json_decode($new_contents, TRUE); + $this->assertEquals( + "$media_url?_format=jsonld", + $json['@graph'][0]['http://www.iana.org/assignments/relation/describedby'][0]['@id'], + 'Missing alter added predicate.' + ); + $this->assertEquals( + $file_url, + $json['@graph'][0]['@id'], + 'Alter did not swap "@id" of media with file url.' + ); + + } + } diff --git a/tests/src/Functional/NodeHasTermTest.php b/tests/src/Functional/NodeHasTermTest.php new file mode 100644 index 00000000..a27cf474 --- /dev/null +++ b/tests/src/Functional/NodeHasTermTest.php @@ -0,0 +1,90 @@ +createImageTag(); + $this->createPreservationMasterTag(); + } + + /** + * @covers \Drupal\islandora\Plugin\Condition\NodeHasTerm + */ + public function testNodeHasTerm() { + + // Create a new node with the tag. + $node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => 'test_type', + 'title' => 'Test Node', + 'field_tags' => [$this->imageTerm->id()], + ]); + + // Create and execute the condition. + $condition_manager = $this->container->get('plugin.manager.condition'); + $condition = $condition_manager->createInstance( + 'node_has_term', + [ + 'uri' => 'http://purl.org/coar/resource_type/c_c513', + ] + ); + $condition->setContextValue('node', $node); + $this->assertTrue($condition->execute(), "Condition should pass if node has the term"); + + // Create a new node without the tag. + $node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => 'test_type', + 'title' => 'Test Node', + ]); + + $condition->setContextValue('node', $node); + $this->assertFalse($condition->execute(), "Condition should fail if the node does not have any terms"); + + // Create a new node with the wrong tag. + $node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => 'test_type', + 'title' => 'Test Node', + 'field_tags' => [$this->preservationMasterTerm->id()], + ]); + + $condition->setContextValue('node', $node); + $this->assertFalse($condition->execute(), "Condition should fail if the node has terms, but not the one we want."); + + // Check for two tags this time. + // Node still only has one. + $condition = $condition_manager->createInstance( + 'node_has_term', + [ + 'uri' => 'http://purl.org/coar/resource_type/c_c513,http://pcdm.org/use#PreservationMasterFile', + ] + ); + $condition->setContextValue('node', $node); + $this->assertFalse($condition->execute(), "Condition should fail if node does not have both terms"); + + // Create a node with both tags. + $node = $this->container->get('entity_type.manager')->getStorage('node')->create([ + 'type' => 'test_type', + 'title' => 'Test Node', + 'field_tags' => [$this->imageTerm->id(), $this->preservationMasterTerm->id()], + ]); + $condition->setContextValue('node', $node); + $this->assertTrue($condition->execute(), "Condition should pass if node has both terms"); + } + +} diff --git a/tests/src/FunctionalJavascript/IntegerWeightTest.php b/tests/src/FunctionalJavascript/IntegerWeightTest.php new file mode 100644 index 00000000..ba3892e6 --- /dev/null +++ b/tests/src/FunctionalJavascript/IntegerWeightTest.php @@ -0,0 +1,201 @@ +adminUser = $this->drupalCreateUser( + [ + 'administer content types', + 'administer node fields', + 'administer node display', + ] + ); + + // Create dummy repo_item type to sort (since we don't have + // repository_object without islandora_defaults). + $type = $this->container->get('entity_type.manager')->getStorage('node_type') + ->create([ + 'type' => 'repo_item', + 'name' => 'Repository Item', + ]); + $type->save(); + $this->container->get('router.builder')->rebuild(); + + $fieldStorage = FieldStorageConfig::create([ + 'fieldName' => static::$fieldName, + 'entity_type' => 'node', + 'type' => static::$fieldType, + ]); + $fieldStorage->save(); + $field = FieldConfig::create([ + 'field_storage' => $fieldStorage, + 'bundle' => 'repo_item', + 'required' => FALSE, + ]); + $field->save(); + + for ($n = 1; $n <= 3; $n++) { + $node = $this->drupalCreateNode([ + 'type' => 'repo_item', + 'title' => "Item $n", + static::$fieldName => $n, + ]); + $node->save(); + $this->nodes[] = $node; + } + + ViewsTestData::createTestViews(get_class($this), ['integer_weight_test_views']); + } + + /** + * Test integer weight selector. + */ + public function testIntegerWeightSelector() { + $this->drupalGet('test-integer-weight'); + $page = $this->getSession()->getPage(); + + $weight_select1 = $page->findField("field_integer_weight[0][weight]"); + $weight_select2 = $page->findField("field_integer_weight[1][weight]"); + $weight_select3 = $page->findField("field_integer_weight[2][weight]"); + + // Are row weight selects hidden? + $this->assertFalse($weight_select1->isVisible()); + $this->assertFalse($weight_select2->isVisible()); + $this->assertFalse($weight_select3->isVisible()); + + // Check that 'Item 2' is feavier than 'Item 1'. + $this->assertGreaterThan($weight_select1->getValue(), $weight_select2->getValue()); + + // Does 'Item 1' preced 'Item 2'? + $this->assertOrderInPage(['Item 1', 'Item 2']); + + // No changes yet, so no warning... + $this->assertSession()->pageTextNotContains('You have unsaved changes.'); + + // Drag and drop 'Item 1' over 'Item 2'. + $dragged = $this->xpath("//tr[@class='draggable'][1]//a[@class='tabledrag-handle']")[0]; + $target = $this->xpath("//tr[@class='draggable'][2]//a[@class='tabledrag-handle']")[0]; + $dragged->dragTo($target); + + // Pause for javascript to do it's thing. + $this->assertJsCondition('jQuery(".tabledrag-changed-warning").is(":visible")'); + + // Look for unsaved changes warning. + $this->assertSession()->pageTextContains('You have unsaved changes.'); + + // 'Item 2' should now preced 'Item 1'. + $this->assertOrderInPage(['Item 2', 'Item 1']); + + $this->submitForm([], 'Save'); + + // Form refresh should reflect the new order still. + $this->assertOrderInPage(['Item 2', 'Item 1']); + + // Ensure the stored values reflect the new order. + $item1 = Node::load($this->nodes[0]->id()); + $item2 = Node::load($this->nodes[1]->id()); + $this->assertGreaterThan($item2->field_integer_weight->getString(), $item1->field_integer_weight->getString()); + } + + /** + * Asserts that several pieces of markup are in a given order in the page. + * + * Taken verbatim from the weight module. + * + * @param string[] $items + * An ordered list of strings. + * + * @throws \Behat\Mink\Exception\ExpectationException + * When any of the given string is not found. + */ + protected function assertOrderInPage(array $items) { + $session = $this->getSession(); + $text = $session->getPage()->getHtml(); + $strings = []; + foreach ($items as $item) { + if (($pos = strpos($text, $item)) === FALSE) { + throw new ExpectationException("Cannot find '$item' in the page", $session->getDriver()); + } + $strings[$pos] = $item; + } + ksort($strings); + $ordered = implode(', ', array_map(function ($item) { + return "'$item'"; + }, $items)); + $this->assertSame($items, array_values($strings), "Found strings, ordered as: $ordered."); + } + +} diff --git a/tests/src/Kernel/EventGeneratorTest.php b/tests/src/Kernel/EventGeneratorTest.php index b33d4cd5..c423cda3 100644 --- a/tests/src/Kernel/EventGeneratorTest.php +++ b/tests/src/Kernel/EventGeneratorTest.php @@ -5,7 +5,7 @@ namespace Drupal\Tests\islandora\Kernel; use Drupal\islandora\EventGenerator\EventGenerator; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; -use Drupal\simpletest\UserCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; /** * Tests the EventGenerator default implementation. @@ -64,7 +64,10 @@ class EventGeneratorTest extends IslandoraKernelTestBase { $this->entity->save(); // Create the event generator so we can test it. - $this->eventGenerator = new EventGenerator(); + $this->eventGenerator = new EventGenerator( + $this->container->get('islandora.utils'), + $this->container->get('islandora.media_source_service') + ); } /** @@ -146,9 +149,12 @@ class EventGeneratorTest extends IslandoraKernelTestBase { foreach ($msg['actor']['url'] as $url) { $this->assertTrue($url['type'] == 'Link', "'url' entries must have type 'Link'"); $this->assertTrue( - $url['mediaType'] == 'application/ld+json' || $url['mediaType'] == 'text/html', - "'url' entries must be either html or jsonld" - ); + in_array( + $url['mediaType'], + ['application/json', 'application/ld+json', 'text/html'] + ), + "'url' entries must be either html, json, or jsonld" + ); } // Make sure the object exists and is a uri. @@ -158,13 +164,16 @@ class EventGeneratorTest extends IslandoraKernelTestBase { $msg["object"]["id"] == "urn:uuid:{$this->entity->uuid()}", "Id must be an URN with entity's UUID" ); - $this->assertTrue(array_key_exists("url", $msg["actor"]), "Object must have 'url' key."); - foreach ($msg['actor']['url'] as $url) { + $this->assertTrue(array_key_exists("url", $msg["object"]), "Object must have 'url' key."); + foreach ($msg['object']['url'] as $url) { $this->assertTrue($url['type'] == 'Link', "'url' entries must have type 'Link'"); $this->assertTrue( - $url['mediaType'] == 'application/ld+json' || $url['mediaType'] == 'text/html', - "'url' entries must be either html or jsonld" - ); + in_array( + $url['mediaType'], + ['application/json', 'application/ld+json', 'text/html'] + ), + "'url' entries must be either html, json, or jsonld" + ); } } diff --git a/tests/src/Kernel/FedoraPluginTest.php b/tests/src/Kernel/FedoraPluginTest.php index 8f85251b..67491507 100644 --- a/tests/src/Kernel/FedoraPluginTest.php +++ b/tests/src/Kernel/FedoraPluginTest.php @@ -31,7 +31,9 @@ class FedoraPluginTest extends IslandoraKernelTestBase { $mime_guesser = $this->prophesize(MimeTypeGuesserInterface::class)->reveal(); - return new Fedora($api, $mime_guesser); + $language_manager = $this->container->get('language_manager'); + + return new Fedora($api, $mime_guesser, $language_manager); } /** diff --git a/tests/src/Kernel/GeminiClientFactoryTest.php b/tests/src/Kernel/GeminiClientFactoryTest.php new file mode 100644 index 00000000..1bbd8e75 --- /dev/null +++ b/tests/src/Kernel/GeminiClientFactoryTest.php @@ -0,0 +1,87 @@ +prophesize(LoggerInterface::class); + $prophecy->notice(Argument::any()); + $this->logger = $prophecy->reveal(); + } + + /** + * @covers ::create + * @expectedException \Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException + */ + public function testNoUrlBlank() { + $prophecy = $this->prophesize(ImmutableConfig::class); + $prophecy->get(Argument::any())->willReturn(''); + $immutConfig = $prophecy->reveal(); + + $prophecy = $this->prophesize(ConfigFactoryInterface::class); + $prophecy->get(Argument::any())->willReturn($immutConfig); + $configFactory = $prophecy->reveal(); + + GeminiClientFactory::create($configFactory, $this->logger); + } + + /** + * @covers ::create + * @expectedException \Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException + */ + public function testNoUrlNull() { + $prophecy = $this->prophesize(ImmutableConfig::class); + $prophecy->get(Argument::any())->willReturn(NULL); + $immutConfig = $prophecy->reveal(); + + $prophecy = $this->prophesize(ConfigFactoryInterface::class); + $prophecy->get(Argument::any())->willReturn($immutConfig); + $configFactory = $prophecy->reveal(); + + GeminiClientFactory::create($configFactory, $this->logger); + } + + /** + * @covers ::create + * @throws \Exception + */ + public function testUrl() { + $prophecy = $this->prophesize(ImmutableConfig::class); + $prophecy->get(Argument::any())->willReturn('http://localhost:8000/gemini'); + $immutConfig = $prophecy->reveal(); + + $prophecy = $this->prophesize(ConfigFactoryInterface::class); + $prophecy->get(Argument::any())->willReturn($immutConfig); + $configFactory = $prophecy->reveal(); + + $this->assertInstanceOf(GeminiClient::class, GeminiClientFactory::create($configFactory, $this->logger)); + } + +} diff --git a/tests/src/Kernel/GeminiLookupTest.php b/tests/src/Kernel/GeminiLookupTest.php new file mode 100644 index 00000000..15edc2e8 --- /dev/null +++ b/tests/src/Kernel/GeminiLookupTest.php @@ -0,0 +1,290 @@ +prophesize(JwtAuth::class); + $this->jwtAuth = $prophecy->reveal(); + + $prophecy = $this->prophesize(LoggerInterface::class); + $this->logger = $prophecy->reveal(); + + $prophecy = $this->prophesize(MediaSourceService::class); + $this->mediaSource = $prophecy->reveal(); + + $prophecy = $this->prophesize(GeminiClient::class); + $this->geminiClient = $prophecy->reveal(); + + $prophecy = $this->prophesize(Client::class); + $this->guzzle = $prophecy->reveal(); + + // Mock up an entity to use (node in this case). + $prophecy = $this->prophesize(EntityInterface::class); + $prophecy->id()->willReturn(1); + $prophecy->getEntityTypeId()->willReturn('node'); + $prophecy->uuid()->willReturn('abc123'); + $this->entity = $prophecy->reveal(); + + // Mock up a media to use. + $prophecy = $this->prophesize(MediaInterface::class); + $prophecy->id()->willReturn(1); + $prophecy->getEntityTypeId()->willReturn('media'); + $prophecy->uuid()->willReturn('abc123'); + $this->media = $prophecy->reveal(); + } + + /** + * Mocks up a gemini client that fails its lookup. + */ + private function mockGeminiClientForFail() { + $prophecy = $this->prophesize(GeminiClient::class); + $prophecy->getUrls(Argument::any(), Argument::any()) + ->willReturn([]); + $this->geminiClient = $prophecy->reveal(); + } + + /** + * Mocks up a gemini client that finds a fedora url. + */ + private function mockGeminiClientForSuccess() { + $prophecy = $this->prophesize(GeminiClient::class); + $prophecy->getUrls(Argument::any(), Argument::any()) + ->willReturn(['drupal' => '', 'fedora' => 'http://localhost:8080/fcrepo/rest/abc123']); + $this->geminiClient = $prophecy->reveal(); + } + + /** + * Mocks up a media source service that finds the source file for a media. + */ + private function mockMediaSourceForSuccess() { + $prophecy = $this->prophesize(FileInterface::class); + $prophecy->uuid()->willReturn('abc123'); + $file = $prophecy->reveal(); + + $prophecy = $this->prophesize(MediaSourceService::class); + $prophecy->getSourceFile(Argument::any()) + ->willReturn($file); + $this->mediaSource = $prophecy->reveal(); + } + + /** + * Make the gemini lookup out of class variables. + */ + private function createGeminiLookup() { + return new GeminiLookup( + $this->geminiClient, + $this->jwtAuth, + $this->mediaSource, + $this->guzzle, + $this->logger + ); + } + + /** + * @covers ::lookup + * @covers ::__construct + */ + public function testEntityNotSaved() { + // Mock an entity that returns a null id. + // That means it's not saved in the db yet. + $prophecy = $this->prophesize(EntityInterface::class); + $prophecy->id()->willReturn(NULL); + $this->entity = $prophecy->reveal(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + NULL, + $gemini_lookup->lookup($this->entity) + ); + } + + /** + * @covers ::lookup + * @covers ::__construct + */ + public function testEntityNotFound() { + $this->mockGeminiClientForFail(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + NULL, + $gemini_lookup->lookup($this->entity) + ); + } + + /** + * @covers ::lookup + * @covers ::__construct + */ + public function testEntityFound() { + $this->mockGeminiClientForSuccess(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + 'http://localhost:8080/fcrepo/rest/abc123', + $gemini_lookup->lookup($this->entity) + ); + } + + /** + * @covers ::lookup + * @covers ::__construct + */ + public function testMediaHasNoSourceFile() { + // Mock a media source service that fails to find + // the source file for a media. + $prophecy = $this->prophesize(MediaSourceService::class); + $prophecy->getSourceFile(Argument::any()) + ->willThrow(new NotFoundHttpException("Media has no source")); + $this->mediaSource = $prophecy->reveal(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + NULL, + $gemini_lookup->lookup($this->media) + ); + } + + /** + * @covers ::lookup + * @covers ::__construct + */ + public function testMediaNotFound() { + $this->mockMediaSourceForSuccess(); + $this->mockGeminiClientForFail(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + NULL, + $gemini_lookup->lookup($this->media) + ); + } + + /** + * @covers ::lookup + * @covers ::__construct + */ + public function testFileFoundButNoDescribedby() { + $this->mockMediaSourceForSuccess(); + $this->mockGeminiClientForSuccess(); + + // Mock up a guzzle client that does not return + // the describedby header. + $prophecy = $this->prophesize(Client::class); + $prophecy->head(Argument::any(), Argument::any()) + ->willReturn(new Response(200, [])); + $this->guzzle = $prophecy->reveal(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + NULL, + $gemini_lookup->lookup($this->media) + ); + } + + /** + * @covers ::lookup + * @covers ::__construct + */ + public function testMediaFound() { + $this->mockMediaSourceForSuccess(); + $this->mockGeminiClientForSuccess(); + + // Mock up a guzzle client that returns + // the describedby header. + $prophecy = $this->prophesize(Client::class); + $prophecy->head(Argument::any(), Argument::any()) + ->willReturn(new Response(200, ['Link' => '; rel="describedby"'])); + $this->guzzle = $prophecy->reveal(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + 'http://localhost:8080/fcrepo/rest/abc123/fcr:metadata', + $gemini_lookup->lookup($this->media) + ); + } + +} diff --git a/tests/src/Kernel/IslandoraKernelTestBase.php b/tests/src/Kernel/IslandoraKernelTestBase.php index 5690bc1f..5a95cb68 100644 --- a/tests/src/Kernel/IslandoraKernelTestBase.php +++ b/tests/src/Kernel/IslandoraKernelTestBase.php @@ -54,6 +54,7 @@ abstract class IslandoraKernelTestBase extends KernelTestBase { $this->installEntitySchema('context'); $this->installEntitySchema('file'); $this->installConfig('filter'); + $this->installConfig('rest'); } } diff --git a/tests/src/Kernel/JwtEventSubscriberTest.php b/tests/src/Kernel/JwtEventSubscriberTest.php index 8fd9e2be..f97eab9f 100644 --- a/tests/src/Kernel/JwtEventSubscriberTest.php +++ b/tests/src/Kernel/JwtEventSubscriberTest.php @@ -7,7 +7,7 @@ use Drupal\jwt\Authentication\Event\JwtAuthValidEvent; use Drupal\jwt\Authentication\Event\JwtAuthValidateEvent; use Drupal\jwt\JsonWebToken\JsonWebToken; use Drupal\jwt\JsonWebToken\JsonWebTokenInterface; -use Drupal\simpletest\UserCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\core\Entity\EntityStorageInterface; use Drupal\islandora\EventSubscriber\JwtEventSubscriber;