From ef2e42170e5836ffe6d13512fe933e049d488a20 Mon Sep 17 00:00:00 2001 From: Paul Pound Date: Tue, 27 Nov 2018 11:17:55 -0400 Subject: [PATCH 01/70] only change view mode for the current node (#104) * only change view mode for the current node * fix coder issue --- islandora.module | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/islandora.module b/islandora.module index 3e06b0af..aacf9029 100644 --- a/islandora.module +++ b/islandora.module @@ -257,23 +257,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."); + } } } } From 4ba1816ed91fcb5f7ab77b56145496fc63332248 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Thu, 29 Nov 2018 10:39:31 -0600 Subject: [PATCH 02/70] Abstract derivative events and bring image/video in-house (#103) * Make image, video as submodules * Fix branch names and Travis pointers * Deduplicate test code * Return the account * Make queue configurable Remove extraneous CONTRIBUTING.md and composer.json --- config/schema/islandora.schema.yml | 12 +- islandora.info.yml | 2 +- modules/islandora_image/.travis.yml | 41 +++ modules/islandora_image/LICENSE | 339 ++++++++++++++++++ modules/islandora_image/README.md | 23 ++ .../config/schema/islandora_image.info.yml | 28 ++ .../islandora_image/islandora_image.info.yml | 7 + .../islandora_image/islandora_image.module | 24 ++ .../Plugin/Action/GenerateImageDerivative.php | 55 +++ .../tests/fixtures/test_file.txt | 1 + .../GenerateImageDerivativeTest.php | 84 +++++ modules/islandora_video/LICENSE | 339 ++++++++++++++++++ modules/islandora_video/README.md | 23 ++ .../config/schema/islandora_video.info.yml | 28 ++ .../islandora_video/islandora_video.info.yml | 7 + .../islandora_video/islandora_video.module | 32 ++ .../Plugin/Action/GenerateVideoDerivative.php | 54 +++ .../tests/fixtures/test_file.txt | 1 + .../GenerateVideoDerivativeTest.php | 82 +++++ .../Action/AbstractGenerateDerivative.php | 301 ++++++++++++++++ .../Functional/GenerateDerivativeTestBase.php | 116 ++++++ .../IslandoraFunctionalTestBase.php | 54 +++ tests/src/Functional/LinkHeaderTest.php | 36 +- 23 files changed, 1649 insertions(+), 40 deletions(-) create mode 100644 modules/islandora_image/.travis.yml create mode 100644 modules/islandora_image/LICENSE create mode 100644 modules/islandora_image/README.md create mode 100644 modules/islandora_image/config/schema/islandora_image.info.yml create mode 100644 modules/islandora_image/islandora_image.info.yml create mode 100644 modules/islandora_image/islandora_image.module create mode 100644 modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php create mode 100644 modules/islandora_image/tests/fixtures/test_file.txt create mode 100644 modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php create mode 100644 modules/islandora_video/LICENSE create mode 100644 modules/islandora_video/README.md create mode 100644 modules/islandora_video/config/schema/islandora_video.info.yml create mode 100644 modules/islandora_video/islandora_video.info.yml create mode 100644 modules/islandora_video/islandora_video.module create mode 100644 modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php create mode 100644 modules/islandora_video/tests/fixtures/test_file.txt create mode 100644 modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php create mode 100644 src/Plugin/Action/AbstractGenerateDerivative.php create mode 100644 tests/src/Functional/GenerateDerivativeTestBase.php diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index ad59a82c..d786a9a5 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -3,13 +3,13 @@ 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' action.configuration.emit_node_event: @@ -68,21 +68,21 @@ condition.plugin.node_has_term: type: condition.plugin mapping: uri: - type: text + type: text label: 'Taxonomy Term URI' 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: 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/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..4e6a36e2 --- /dev/null +++ b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php @@ -0,0 +1,55 @@ +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..69e18ba7 --- /dev/null +++ b/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php @@ -0,0 +1,84 @@ +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_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..81d5ac7c --- /dev/null +++ b/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php @@ -0,0 +1,54 @@ +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..64bd6d4a --- /dev/null +++ b/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php @@ -0,0 +1,82 @@ +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/Plugin/Action/AbstractGenerateDerivative.php b/src/Plugin/Action/AbstractGenerateDerivative.php new file mode 100644 index 00000000..d7acc5a2 --- /dev/null +++ b/src/Plugin/Action/AbstractGenerateDerivative.php @@ -0,0 +1,301 @@ +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' => '', + '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'] = $source_file->url('canonical', ['absolute' => TRUE]); + + // 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' => $source_media->bundle(), + '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']); + + 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['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'), '\\/'); + } + +} diff --git a/tests/src/Functional/GenerateDerivativeTestBase.php b/tests/src/Functional/GenerateDerivativeTestBase.php new file mode 100644 index 00000000..b650ca3b --- /dev/null +++ b/tests/src/Functional/GenerateDerivativeTestBase.php @@ -0,0 +1,116 @@ +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..733d24a6 100644 --- a/tests/src/Functional/IslandoraFunctionalTestBase.php +++ b/tests/src/Functional/IslandoraFunctionalTestBase.php @@ -37,6 +37,20 @@ class IslandoraFunctionalTestBase extends BrowserTestBase { 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} */ @@ -161,6 +175,46 @@ EOD; $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. */ 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([ From 4ce5e44c29a3500198237335fa7b7d25fbdabeab Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Wed, 12 Dec 2018 13:44:08 -0600 Subject: [PATCH 03/70] Make derivative Drupal media type configurable (#106) * Make derivative media type configurable * Unset parameter or Alpaca vomits * Coder * More coder --- .../Plugin/Action/GenerateImageDerivative.php | 1 + .../Plugin/Action/GenerateVideoDerivative.php | 1 + .../Action/AbstractGenerateDerivative.php | 38 ++++++++++++++++++- .../FormDisplayAlterReactionTest.php | 3 +- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php index 4e6a36e2..fa713a71 100644 --- a/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php +++ b/modules/islandora_image/src/Plugin/Action/GenerateImageDerivative.php @@ -23,6 +23,7 @@ class GenerateImageDerivative extends AbstractGenerateDerivative { $config = parent::defaultConfiguration(); $config['mimetype'] = 'image/jpeg'; $config['path'] = '[date:custom:Y]-[date:custom:m]/[node:nid].jpg'; + $config['destination_media_type'] = 'image'; return $config; } diff --git a/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php b/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php index 81d5ac7c..c28ffdc0 100644 --- a/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php +++ b/modules/islandora_video/src/Plugin/Action/GenerateVideoDerivative.php @@ -24,6 +24,7 @@ class GenerateVideoDerivative extends AbstractGenerateDerivative { $config['path'] = '[date:custom:Y]-[date:custom:m]/[node:nid].mp4'; $config['mimetype'] = 'video/mp4'; $config['queue'] = 'islandora-connector-homarus'; + $config['destination_media_type'] = 'video'; return $config; } diff --git a/src/Plugin/Action/AbstractGenerateDerivative.php b/src/Plugin/Action/AbstractGenerateDerivative.php index d7acc5a2..4635a95c 100644 --- a/src/Plugin/Action/AbstractGenerateDerivative.php +++ b/src/Plugin/Action/AbstractGenerateDerivative.php @@ -126,6 +126,7 @@ class AbstractGenerateDerivative extends EmitEvent { 'derivative_term_uri' => '', 'mimetype' => '', 'args' => '', + 'destination_media_type' => '', 'scheme' => file_default_scheme(), 'path' => '[date:custom:Y]-[date:custom:m]/[node:nid].bin', ]; @@ -165,7 +166,7 @@ class AbstractGenerateDerivative extends EmitEvent { $route_params = [ 'node' => $entity->id(), - 'media_type' => $source_media->bundle(), + '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) @@ -186,6 +187,7 @@ class AbstractGenerateDerivative extends EmitEvent { unset($data['derivative_term_uri']); unset($data['path']); unset($data['scheme']); + unset($data['destination_media_type']); return $data; } @@ -216,6 +218,14 @@ class AbstractGenerateDerivative extends EmitEvent { '#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'), @@ -296,6 +306,32 @@ class AbstractGenerateDerivative extends EmitEvent { $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/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 { } } - From 9c1b702d42a3ae114c8ed7867727996702766dea Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Tue, 18 Dec 2018 08:51:47 -0800 Subject: [PATCH 04/70] Add userid option to migrate:import (#108) * add userid option to migrate:import * coding standards --- drush.services.yml | 5 ++ src/Commands/IslandoraCommands.php | 82 ++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 drush.services.yml create mode 100644 src/Commands/IslandoraCommands.php 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/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(); + } + } + +} From 4c41a64631dfafdc7159fd38592c808f85b2655d Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Wed, 19 Dec 2018 11:15:23 -0800 Subject: [PATCH 05/70] Allow JSON-LD @Type to be configurable based on a specified Content Entity field (#107) * new EntityBundle condition and JsonldTypeAlterReaction * coding standards and removed logger notice * add description to formbuilder * support referenced entities that include a field_external_uri --- src/Plugin/Condition/EntityBundle.php | 96 +++++++++++++++++++ .../JsonldTypeAlterReaction.php | 92 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/Plugin/Condition/EntityBundle.php create mode 100644 src/Plugin/ContextReaction/JsonldTypeAlterReaction.php diff --git a/src/Plugin/Condition/EntityBundle.php b/src/Plugin/Condition/EntityBundle.php new file mode 100644 index 00000000..5ec1a19c --- /dev/null +++ b/src/Plugin/Condition/EntityBundle.php @@ -0,0 +1,96 @@ +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 (\Drupal::routeMatch()->getParameters()->keys() as $type) { + if ($this->getContext($type)->hasContextValue()) { + $entity = $this->getContextValue($type); + 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/ContextReaction/JsonldTypeAlterReaction.php b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php new file mode 100644 index 00000000..e130a68e --- /dev/null +++ b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php @@ -0,0 +1,92 @@ +t('Alter JSON-LD Type context reaction.'); + } + + /** + * {@inheritdoc} + */ + public function execute(EntityInterface $entity = NULL, array &$normalized = NULL, array $context = NULL) { + $config = $this->getConfiguration(); + // Use a pre-configured field as the source of the additional @type. + if (($entity->hasField($config['source_field'])) && + (!empty($entity->get($config['source_field'])->getValue()))) { + if (isset($normalized['@graph']) && is_array($normalized['@graph'])) { + foreach ($normalized['@graph'] as &$graph) { + 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) { + $graph['@type'][] = $value['uri']; + } + } + } + else { + $graph['@type'][] = NormalizerBase::escapePrefix($type['value'], $context['namespaces']); + } + } + } + } + } + } + + /** + * {@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')]); + } + +} From 0d4d45b8c8f3d02aabae0c33217a3d03ebf85a86 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Fri, 21 Dec 2018 13:54:22 -0400 Subject: [PATCH 06/70] Linked agents (#105) * Features export for https://github.com/Islandora-CLAW/CLAW/issues/905 * removing islandora_demo as a dependency --- .../install/views.view.display_media.yml | 78 +++++++++++++++++++ .../islandora_core_feature.features.yml | 0 .../islandora_core_feature.info.yml | 0 3 files changed, 78 insertions(+) mode change 100644 => 100755 modules/islandora_core_feature/islandora_core_feature.features.yml mode change 100644 => 100755 modules/islandora_core_feature/islandora_core_feature.info.yml 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/islandora_core_feature.features.yml b/modules/islandora_core_feature/islandora_core_feature.features.yml old mode 100644 new mode 100755 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 From 57070658accb9e0cf553ece64d28e03891ba9f3a Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Thu, 3 Jan 2019 10:58:25 -0800 Subject: [PATCH 07/70] Add tests for the `EntityBundle` and `JsonldAlterTypeReaction` plugins (Issue 992) (#110) * EntityBundleTest WIP * don't use route parameters to identify entity * give taxonomy permissions and fix 'Hello World!' NOT in output * WIP JsonldTypeAlterReactionTest * working JsonldTypeAlterReactionTest - remove buggy FieldUiTestTrait - use schema:Organization for test predicate - remove commented-out code - add assertTrue for successful @type modification * restore media type comment --- src/Plugin/Condition/EntityBundle.php | 10 +-- tests/src/Functional/EntityBundleTest.php | 43 +++++++++ .../IslandoraFunctionalTestBase.php | 10 ++- .../JsonldTypeAlterReactionTest.php | 87 +++++++++++++++++++ 4 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 tests/src/Functional/EntityBundleTest.php create mode 100644 tests/src/Functional/JsonldTypeAlterReactionTest.php diff --git a/src/Plugin/Condition/EntityBundle.php b/src/Plugin/Condition/EntityBundle.php index 5ec1a19c..96fa76d1 100644 --- a/src/Plugin/Condition/EntityBundle.php +++ b/src/Plugin/Condition/EntityBundle.php @@ -12,8 +12,8 @@ use Drupal\Core\Form\FormStateInterface; * id = "entity_bundle", * label = @Translation("Entity Bundle"), * context = { - * "node" = @ContextDefinition("entity:node", label = @Translation("Node")), - * "taxonomy_term" = @ContextDefinition("entity:taxonomy_term", label = @Translation("Term")) + * "node" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Node")), + * "taxonomy_term" = @ContextDefinition("entity:taxonomy_term", required = FALSE, label = @Translation("Term")) * } * ) */ @@ -56,9 +56,9 @@ class EntityBundle extends ConditionPluginBase { * {@inheritdoc} */ public function evaluate() { - foreach (\Drupal::routeMatch()->getParameters()->keys() as $type) { - if ($this->getContext($type)->hasContextValue()) { - $entity = $this->getContextValue($type); + foreach ($this->getContexts() as $context) { + if ($context->hasContextValue()) { + $entity = $context->getContextValue(); if (!empty($this->configuration['bundles'][$entity->bundle()])) { return !$this->isNegated(); } 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/IslandoraFunctionalTestBase.php b/tests/src/Functional/IslandoraFunctionalTestBase.php index 733d24a6..a689db78 100644 --- a/tests/src/Functional/IslandoraFunctionalTestBase.php +++ b/tests/src/Functional/IslandoraFunctionalTestBase.php @@ -20,7 +20,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase { use TestFileCreationTrait; use MediaFunctionalTestCreateMediaTypeTrait; - protected static $modules = ['context_ui', 'islandora']; + protected static $modules = ['context_ui', 'field_ui', 'islandora']; protected static $configSchemaCheckerExclusions = [ 'jwt.config', @@ -250,6 +250,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/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' + ); + } + +} From 661e3f8580abad4c28fc4de0d11235b6f406f770 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 3 Jan 2019 16:09:47 -0400 Subject: [PATCH 08/70] Adding rdf mapping for islandora_display (#109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 👍 --- ...apping.taxonomy_term.islandora_display.yml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100755 modules/islandora_core_feature/config/install/rdf.mapping.taxonomy_term.islandora_display.yml 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 100755 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' From c002215cdb3ad2f003da3d86c90b928b2481e911 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 10 Jan 2019 11:25:28 -0400 Subject: [PATCH 09/70] Seting chullo dependency to an actual version and not dev-master (#111) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1923cff9..f5eb4df0 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "islandora/chullo" : "dev-master" + "islandora/chullo" : "^0.2.0" }, "require-dev": { "phpunit/phpunit": "^6", From e11363b0cfbaf9b9767dad1814dcc1d77f1ba1c7 Mon Sep 17 00:00:00 2001 From: Natkeeran Date: Fri, 8 Feb 2019 10:26:52 -0500 Subject: [PATCH 10/70] islandora_audio plugin module (#114) * islandora_audio plugin module * changes requested by Danny --- modules/islandora_audio/LICENSE | 339 ++++++++++++++++++ modules/islandora_audio/README.md | 23 ++ .../config/schema/islandora_audio.info.yml | 28 ++ .../islandora_audio/islandora_audio.info.yml | 7 + .../islandora_audio/islandora_audio.module | 32 ++ .../Plugin/Action/GenerateAudioDerivative.php | 56 +++ .../tests/fixtures/test_file.txt | 1 + .../GenerateAudioDerivativeTest.php | 82 +++++ 8 files changed, 568 insertions(+) create mode 100644 modules/islandora_audio/LICENSE create mode 100644 modules/islandora_audio/README.md create mode 100644 modules/islandora_audio/config/schema/islandora_audio.info.yml create mode 100644 modules/islandora_audio/islandora_audio.info.yml create mode 100644 modules/islandora_audio/islandora_audio.module create mode 100644 modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php create mode 100644 modules/islandora_audio/tests/fixtures/test_file.txt create mode 100644 modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php 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..c103bb56 --- /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..9c7f4e87 --- /dev/null +++ b/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php @@ -0,0 +1,82 @@ +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); + } + +} From 96634a3de87975458a0dfcfccb9e33625eebb33a Mon Sep 17 00:00:00 2001 From: Natkeeran Date: Fri, 8 Feb 2019 16:01:46 -0500 Subject: [PATCH 11/70] specific service file path format is needed! (#115) --- .../src/Plugin/Action/GenerateAudioDerivative.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php b/modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php index c103bb56..41e33d55 100644 --- a/modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php +++ b/modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php @@ -21,7 +21,7 @@ class GenerateAudioDerivative extends AbstractGenerateDerivative { */ public function defaultConfiguration() { $config = parent::defaultConfiguration(); - $config['path'] = '[date:custom:Y]-[date:custom:m]/[node:nid].mp3'; + $config['path'] = '[date:custom:Y]-[date:custom:m]/[node:nid]-[term:name].mp3'; $config['mimetype'] = 'audio/mpeg'; $config['queue'] = 'islandora-connector-homarus'; $config['destination_media_type'] = 'audio'; From 9e0a2e546c53cb9b6f7baa4d329dc098705bef29 Mon Sep 17 00:00:00 2001 From: J Hunt Date: Thu, 28 Feb 2019 10:20:44 +1300 Subject: [PATCH 12/70] #1028 Fix member of, media of prepopulate via URL query (#117) --- src/Controller/ManageMembersController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()]] ), ]; } From 2bfefc2bdf1854566ae86ac2e341381088561449 Mon Sep 17 00:00:00 2001 From: J Hunt Date: Wed, 6 Mar 2019 03:04:00 +1300 Subject: [PATCH 13/70] Fix typos. (#121) --- src/EventSubscriber/NodeLinkHeaderSubscriber.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventSubscriber/NodeLinkHeaderSubscriber.php b/src/EventSubscriber/NodeLinkHeaderSubscriber.php index 6ce958b4..e49b4cc1 100644 --- a/src/EventSubscriber/NodeLinkHeaderSubscriber.php +++ b/src/EventSubscriber/NodeLinkHeaderSubscriber.php @@ -95,7 +95,7 @@ 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 = []; From f7f143754e48e80373da873ac2fdf842b8db705b Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Wed, 6 Mar 2019 13:01:06 -0800 Subject: [PATCH 14/70] Modifies Guzzle client handler to generate a new JWT for each request. (#119) --- src/Flysystem/Fedora.php | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Flysystem/Fedora.php b/src/Flysystem/Fedora.php index 772e89e6..fadd8a0f 100644 --- a/src/Flysystem/Fedora.php +++ b/src/Flysystem/Fedora.php @@ -7,6 +7,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; 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; @@ -48,13 +49,10 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac * {@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'], @@ -71,22 +69,19 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac /** * 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); }; }; From 92df5dc173b4e99494462bae871050432ebcf441 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 7 Mar 2019 12:59:35 -0400 Subject: [PATCH 15/70] Missing a fclose :S (#120) --- src/MediaSource/MediaSourceService.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MediaSource/MediaSourceService.php b/src/MediaSource/MediaSourceService.php index 2044ab00..af23c26a 100644 --- a/src/MediaSource/MediaSourceService.php +++ b/src/MediaSource/MediaSourceService.php @@ -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"); } From fb1802b27d1ccf4a0910deea9db14c1d7df9917e Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Fri, 8 Mar 2019 16:16:08 -0800 Subject: [PATCH 16/70] Make JWT Expiry configurable - Issue 1030 (#116) * Make JWT expiry configurable using Islandora Core Setting form. * coding standards --- src/EventSubscriber/JwtEventSubscriber.php | 6 +++++- src/Form/IslandoraSettingsForm.php | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php index 60859782..730c1d25 100644 --- a/src/Form/IslandoraSettingsForm.php +++ b/src/Form/IslandoraSettingsForm.php @@ -15,6 +15,7 @@ class IslandoraSettingsForm extends ConfigFormBase { const CONFIG_NAME = 'islandora.settings'; const BROKER_URL = 'broker_url'; + const JWT_EXPIRY = 'jwt_expiry'; /** * {@inheritdoc} @@ -44,6 +45,12 @@ class IslandoraSettingsForm extends ConfigFormBase { '#default_value' => $config->get(self::BROKER_URL) ? $config->get(self::BROKER_URL) : 'tcp://localhost:61613', ]; + $form[self::JWT_EXPIRY] = [ + '#type' => 'textfield', + '#title' => $this->t('JWT Expiry'), + '#default_value' => $config->get(self::JWT_EXPIRY) ? $config->get(self::JWT_EXPIRY) : '+2 hour', + ]; + return parent::buildForm($form, $form_state); } @@ -74,6 +81,18 @@ 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( + '"@exipry" is not a valid time or interval expression.', + ['@expiry' => $expiry] + ) + ); + } } /** @@ -84,6 +103,7 @@ class IslandoraSettingsForm extends ConfigFormBase { $config ->set(self::BROKER_URL, $form_state->getValue(self::BROKER_URL)) + ->set(self::JWT_EXPIRY, $form_state->getValue(self::JWT_EXPIRY)) ->save(); parent::submitForm($form, $form_state); From 64d2ff0d0e37eb2f60556bf49d8450891d755d25 Mon Sep 17 00:00:00 2001 From: Mark Jordan Date: Fri, 8 Mar 2019 16:17:26 -0800 Subject: [PATCH 17/70] Work on https://github.com/Islandora-CLAW/CLAW/issues/1014. (#113) * Work on https://github.com/Islandora-CLAW/CLAW/issues/1014. * Fix coding style. * Use $scheme, not 'fedora'. --- src/Plugin/Action/EmitFileEvent.php | 5 +++++ 1 file changed, 5 insertions(+) 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; From be09b4340a72e31272ef855c2046817243907ce3 Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Tue, 12 Mar 2019 11:59:04 -0700 Subject: [PATCH 18/70] Bumps composer versions as per Issue 1052 (#122) * update migrate plugins * update prepopulate module --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index f5eb4df0..c4d6666f 100644 --- a/composer.json +++ b/composer.json @@ -21,11 +21,11 @@ "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", From a209d3c1ca4ad248a43210d724f0106ebe428794 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Mon, 18 Mar 2019 16:20:18 -0300 Subject: [PATCH 19/70] Adding media to the EntityBundle condition (#123) --- src/Plugin/Condition/EntityBundle.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Plugin/Condition/EntityBundle.php b/src/Plugin/Condition/EntityBundle.php index 96fa76d1..77cf2826 100644 --- a/src/Plugin/Condition/EntityBundle.php +++ b/src/Plugin/Condition/EntityBundle.php @@ -13,6 +13,7 @@ use Drupal\Core\Form\FormStateInterface; * label = @Translation("Entity Bundle"), * context = { * "node" = @ContextDefinition("entity:node", required = FALSE, label = @Translation("Node")), + * "media" = @ContextDefinition("entity:media", required = FALSE, label = @Translation("Media")), * "taxonomy_term" = @ContextDefinition("entity:taxonomy_term", required = FALSE, label = @Translation("Term")) * } * ) @@ -24,7 +25,7 @@ class EntityBundle extends ConditionPluginBase { */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { $options = []; - foreach (['node', 'taxonomy_term'] as $content_entity) { + foreach (['node', 'media', 'taxonomy_term'] as $content_entity) { $bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo($content_entity); foreach ($bundles as $bundle => $bundle_properties) { $options[$bundle] = $this->t('@bundle (@type)', [ From a82f80f3ead45399722e329873eaa04e9ff9a81d Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Tue, 30 Apr 2019 10:09:03 -0500 Subject: [PATCH 20/70] Display Fedora URI in Drupal (#124) * Make a pseudo field we can use * coder * Fix config and coder for root level * Default to empty array * Add tests and clean up some deprecations * Form validation of Gemini URI before bundle selection. * Coder * Add functional test for Islandora Settings --- composer.json | 2 +- config/schema/islandora.schema.yml | 12 ++ islandora.module | 56 ++++++++ islandora.services.yml | 7 + src/Form/IslandoraSettingsForm.php | 117 ++++++++++++++++- src/GeminiClientFactory.php | 47 +++++++ src/GeminiLookup.php | 101 +++++++++++++++ .../IslandoraFunctionalTestBase.php | 8 +- .../Functional/IslandoraSettingsFormTest.php | 61 +++++++++ tests/src/Kernel/EventGeneratorTest.php | 2 +- tests/src/Kernel/GeminiClientFactoryTest.php | 82 ++++++++++++ tests/src/Kernel/GeminiLookupTest.php | 121 ++++++++++++++++++ tests/src/Kernel/JwtEventSubscriberTest.php | 2 +- 13 files changed, 609 insertions(+), 9 deletions(-) create mode 100644 src/GeminiClientFactory.php create mode 100644 src/GeminiLookup.php create mode 100644 tests/src/Functional/IslandoraSettingsFormTest.php create mode 100644 tests/src/Kernel/GeminiClientFactoryTest.php create mode 100644 tests/src/Kernel/GeminiLookupTest.php diff --git a/composer.json b/composer.json index c4d6666f..0429cf69 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "islandora/chullo" : "^0.2.0" + "islandora/crayfish-commons": "^0.0" }, "require-dev": { "phpunit/phpunit": "^6", diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index d786a9a5..a2ca48d3 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -11,6 +11,18 @@ islandora.settings: broadcast_queue: 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 diff --git a/islandora.module b/islandora.module index aacf9029..34bbc5fb 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; @@ -344,3 +348,55 @@ function islandora_form_block_form_alter(&$form, FormStateInterface $form_state, unset($form['visibility']['node_has_term']); unset($form['visibility']['media_uses_filesystem']); } + +/** + * 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) ? $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) { + if ($view_mode == 'full') { + 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), + ], + ], + ]; + } + } + } + } +} diff --git a/islandora.services.yml b/islandora.services.yml index 6f6b85c7..1a34ed37 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -51,3 +51,10 @@ services: islandora.utils: class: Drupal\islandora\IslandoraUtils arguments: ['@entity_type.manager', '@entity_field.manager', '@entity.query', '@context.manager', '@flysystem_factory'] + 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', '@logger.channel.islandora'] diff --git a/src/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php index 730c1d25..6ec80165 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. @@ -16,6 +22,38 @@ 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} @@ -51,6 +89,37 @@ class IslandoraSettingsForm extends ConfigFormBase { '#default_value' => $config->get(self::JWT_EXPIRY) ? $config->get(self::JWT_EXPIRY) : '+2 hour', ]; + $form[self::GEMINI_URL] = [ + '#type' => 'textfield', + '#title' => $this->t('Gemini URL'), + '#default_value' => $config->get(self::GEMINI_URL) ? $config->get(self::GEMINI_URL) : '', + ]; + + $selected_bundles = $config->get(self::GEMINI_PSEUDO) ? $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); } @@ -88,22 +157,66 @@ class IslandoraSettingsForm extends ConfigFormBase { $form_state->setErrorByName( self::JWT_EXPIRY, $this->t( - '"@exipry" is not a valid time or interval expression.', + '"@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..b10471b0 --- /dev/null +++ b/src/GeminiLookup.php @@ -0,0 +1,101 @@ +geminiClient = $client; + $this->jwtProvider = $jwt_auth; + $this->logger = $logger; + } + + /** + * Static creator. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The container. + * + * @return \Drupal\islandora\GeminiLookup + * A GeminiLookup service. + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('islandora.gemini_client'), + $container->get('jwt.authentication.jwt'), + $container->get('logger.channel.islandora') + ); + } + + /** + * 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 + * + * @throws \Drupal\Core\Entity\EntityMalformedException + * If the entity cannot be converted to a URL. + */ + public function lookup(EntityInterface $entity) { + if ($entity->id() != NULL) { + $drupal_uri = $entity->toUrl()->setAbsolute()->toString(); + $drupal_uri .= '?_format=jsonld'; + $token = "Bearer " . $this->jwtProvider->generateToken(); + $linked_uri = $this->geminiClient->findByUri($drupal_uri, $token); + if (!is_null($linked_uri)) { + if (is_array($linked_uri)) { + $linked_uri = reset($linked_uri); + } + return $linked_uri; + } + } + // Return null if we weren't in a saved entity or we didn't find a uri. + return NULL; + } + +} diff --git a/tests/src/Functional/IslandoraFunctionalTestBase.php b/tests/src/Functional/IslandoraFunctionalTestBase.php index a689db78..e1d6e91f 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,7 +18,7 @@ class IslandoraFunctionalTestBase extends BrowserTestBase { use EntityReferenceTestTrait; use TestFileCreationTrait; - use MediaFunctionalTestCreateMediaTypeTrait; + use MediaTypeCreationTrait; protected static $modules = ['context_ui', 'field_ui', 'islandora']; @@ -157,7 +157,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); 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/Kernel/EventGeneratorTest.php b/tests/src/Kernel/EventGeneratorTest.php index b33d4cd5..9f0bed06 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. diff --git a/tests/src/Kernel/GeminiClientFactoryTest.php b/tests/src/Kernel/GeminiClientFactoryTest.php new file mode 100644 index 00000000..3259c221 --- /dev/null +++ b/tests/src/Kernel/GeminiClientFactoryTest.php @@ -0,0 +1,82 @@ +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..9c96b6c2 --- /dev/null +++ b/tests/src/Kernel/GeminiLookupTest.php @@ -0,0 +1,121 @@ +prophesize(JwtAuth::class); + $prophecy->generateToken()->willReturn("islandora"); + $this->jwtAuth = $prophecy->reveal(); + + $prophecy = $this->prophesize(LoggerInterface::class); + $this->logger = $prophecy->reveal(); + + $prophecy = $this->prophesize(GeminiClient::class); + $prophecy->findByUri(Argument::any(), Argument::any())->willReturn(NULL); + $this->geminiClient = $prophecy->reveal(); + } + + /** + * @covers ::lookup + * @covers ::__construct + * @throws \Drupal\Core\Entity\EntityMalformedException + */ + public function testEntityNotSaved() { + $prophecy = $this->prophesize(EntityInterface::class); + $prophecy->id()->willReturn(NULL); + $entity = $prophecy->reveal(); + $this->geminiLookup = new GeminiLookup( + $this->geminiClient, + $this->jwtAuth, + $this->logger + ); + $this->assertEquals(NULL, $this->geminiLookup->lookup($entity)); + } + + /** + * @covers ::lookup + * @covers ::__construct + * @throws \Drupal\Core\Entity\EntityMalformedException + */ + public function testEntityNotFound() { + $prop1 = $this->prophesize(Url::class); + $prop1->toString()->willReturn("http://localhost:8000/node/456"); + + $prop2 = $this->prophesize(Url::class); + $prop2->setAbsolute()->willReturn($prop1->reveal()); + $url = $prop2->reveal(); + + $prophecy = $this->prophesize(EntityInterface::class); + $prophecy->id()->willReturn(456); + $prophecy->toUrl()->willReturn($url); + $entity = $prophecy->reveal(); + + $this->geminiLookup = new GeminiLookup( + $this->geminiClient, + $this->jwtAuth, + $this->logger + ); + + $this->assertEquals(NULL, $this->geminiLookup->lookup($entity)); + } + + /** + * @covers ::lookup + * @covers ::__construct + * @throws \Drupal\Core\Entity\EntityMalformedException + */ + public function testEntityFound() { + $prop1 = $this->prophesize(Url::class); + $prop1->toString()->willReturn("http://localhost:8000/node/456"); + + $prop2 = $this->prophesize(Url::class); + $prop2->setAbsolute()->willReturn($prop1->reveal()); + $url = $prop2->reveal(); + + $prophecy = $this->prophesize(EntityInterface::class); + $prophecy->id()->willReturn(456); + $prophecy->toUrl()->willReturn($url); + $entity = $prophecy->reveal(); + + $prophecy = $this->prophesize(GeminiClient::class); + $prophecy->findByUri(Argument::any(), Argument::any())->willReturn(["http://fedora:8080/some/uri"]); + $this->geminiClient = $prophecy->reveal(); + + $this->geminiLookup = new GeminiLookup( + $this->geminiClient, + $this->jwtAuth, + $this->logger + ); + + $this->assertEquals("http://fedora:8080/some/uri", $this->geminiLookup->lookup($entity)); + } + +} 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; From f3644dca17436e2ccfab8cefd257be19db7c5e5b Mon Sep 17 00:00:00 2001 From: dannylamb Date: Tue, 30 Apr 2019 15:18:01 -0300 Subject: [PATCH 21/70] Config for external content (#125) --- .../rdf.mapping.taxonomy_term.islandora_display.yml | 0 ...action.delete_file_as_fedora_external_content.yml | 12 ++++++++++++ .../system.action.delete_media_from_triplestore.yml | 2 +- .../system.action.delete_node_from_fedora.yml | 2 +- .../system.action.delete_node_from_triplestore.yml | 2 +- ....action.index_file_as_fedora_external_content.yml | 12 ++++++++++++ .../install/system.action.index_file_in_fedora.yml | 2 +- .../install/system.action.index_media_in_fedora.yml | 2 +- .../system.action.index_media_in_triplestore.yml | 2 +- .../install/system.action.index_node_in_fedora.yml | 2 +- .../system.action.index_node_in_triplestore.yml | 2 +- 11 files changed, 32 insertions(+), 8 deletions(-) mode change 100755 => 100644 modules/islandora_core_feature/config/install/rdf.mapping.taxonomy_term.islandora_display.yml create mode 100644 modules/islandora_core_feature/config/install/system.action.delete_file_as_fedora_external_content.yml create mode 100644 modules/islandora_core_feature/config/install/system.action.index_file_as_fedora_external_content.yml 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 old mode 100755 new mode 100644 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 From 43b971cdf627403d7e3436c79161c06ef8eb26e9 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Mon, 6 May 2019 12:33:02 -0300 Subject: [PATCH 22/70] Slideshow image formatter (#127) * Custom image formatter with URL to node instead of media * Tests --- config/schema/islandora.schema.yml | 11 ++ .../IslandoraImageFormatter.php | 136 ++++++++++++++++++ .../IslandoraImageFormatterTest.php | 93 ++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 src/Plugin/Field/FieldFormatter/IslandoraImageFormatter.php create mode 100644 tests/src/Functional/IslandoraImageFormatterTest.php diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index a2ca48d3..1df5b7e8 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -120,3 +120,14 @@ condition.plugin.content_entity_type: type: sequence sequence: type: string + +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/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/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.'); + } + +} From b4b8fe1e121fe62ffec0280fe08e890f4adb885b Mon Sep 17 00:00:00 2001 From: dannylamb Date: Tue, 7 May 2019 11:49:52 -0300 Subject: [PATCH 23/70] Media Fedora Uri Pseudo-Fields (#126) * Getting media fedora uris in the pseudo field as well * Same tests, less code --- islandora.services.yml | 2 +- src/GeminiLookup.php | 125 ++++++++++---- tests/src/Kernel/GeminiLookupTest.php | 230 ++++++++++++++++++++------ 3 files changed, 276 insertions(+), 81 deletions(-) diff --git a/islandora.services.yml b/islandora.services.yml index 1a34ed37..0ba96794 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -57,4 +57,4 @@ services: arguments: ['@config.factory', '@logger.channel.islandora'] islandora.gemini.lookup: class: Drupal\islandora\GeminiLookup - arguments: ['@islandora.gemini.client', '@jwt.authentication.jwt', '@logger.channel.islandora'] + arguments: ['@islandora.gemini.client', '@jwt.authentication.jwt', '@islandora.media_source_service', '@http_client', '@logger.channel.islandora'] diff --git a/src/GeminiLookup.php b/src/GeminiLookup.php index b10471b0..20bd42e0 100644 --- a/src/GeminiLookup.php +++ b/src/GeminiLookup.php @@ -3,10 +3,14 @@ namespace Drupal\islandora; use Drupal\Core\Entity\EntityInterface; +use Drupal\islandora\MediaSource\MediaSourceService; use Drupal\jwt\Authentication\Provider\JwtAuth; +use GuzzleHttp\Psr7; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use Islandora\Crayfish\Commons\Client\GeminiClient; use Psr\Log\LoggerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Locates the matching Fedora URI from the Gemini database. @@ -29,6 +33,20 @@ class GeminiLookup { */ private $jwtProvider; + /** + * A MediaSourceService. + * + * @var \Drupal\islandora\MediaSource\MediaSourceService + */ + private $mediaSource; + + /** + * An http client. + * + * @var \GuzzleHttp\Client + */ + private $guzzle; + /** * The islandora logger channel. * @@ -43,32 +61,27 @@ class GeminiLookup { * The Gemini client. * @param \Drupal\jwt\Authentication\Provider\JwtAuth $jwt_auth * The JWT provider. + * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source + * Media source service. + * @param \GuzzleHttp\Client $guzzle + * Guzzle client. * @param \Psr\Log\LoggerInterface $logger * The Islandora logger. */ - public function __construct(GeminiClient $client, JwtAuth $jwt_auth, LoggerInterface $logger) { + public function __construct( + GeminiClient $client, + JwtAuth $jwt_auth, + MediaSourceService $media_source, + Client $guzzle, + LoggerInterface $logger + ) { $this->geminiClient = $client; $this->jwtProvider = $jwt_auth; + $this->mediaSource = $media_source; + $this->guzzle = $guzzle; $this->logger = $logger; } - /** - * Static creator. - * - * @param \Symfony\Component\DependencyInjection\ContainerInterface $container - * The container. - * - * @return \Drupal\islandora\GeminiLookup - * A GeminiLookup service. - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('islandora.gemini_client'), - $container->get('jwt.authentication.jwt'), - $container->get('logger.channel.islandora') - ); - } - /** * Lookup this entity's URI in the Gemini db and return the other URI. * @@ -77,24 +90,72 @@ class GeminiLookup { * * @return string|null * Return the URI or null - * - * @throws \Drupal\Core\Entity\EntityMalformedException - * If the entity cannot be converted to a URL. */ public function lookup(EntityInterface $entity) { - if ($entity->id() != NULL) { - $drupal_uri = $entity->toUrl()->setAbsolute()->toString(); - $drupal_uri .= '?_format=jsonld'; - $token = "Bearer " . $this->jwtProvider->generateToken(); - $linked_uri = $this->geminiClient->findByUri($drupal_uri, $token); - if (!is_null($linked_uri)) { - if (is_array($linked_uri)) { - $linked_uri = reset($linked_uri); + // 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], '<>'); } - return $linked_uri; } } - // Return null if we weren't in a saved entity or we didn't find a uri. + 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/tests/src/Kernel/GeminiLookupTest.php b/tests/src/Kernel/GeminiLookupTest.php index 9c96b6c2..600fea73 100644 --- a/tests/src/Kernel/GeminiLookupTest.php +++ b/tests/src/Kernel/GeminiLookupTest.php @@ -3,12 +3,17 @@ namespace Drupal\Tests\islandora\Kernel; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Url; +use Drupal\file\FileInterface; +use Drupal\media\MediaInterface; use Drupal\islandora\GeminiLookup; +use Drupal\islandora\MediaSource\MediaSourceService; use Drupal\jwt\Authentication\Provider\JwtAuth; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Response; use Islandora\Crayfish\Commons\Client\GeminiClient; use Prophecy\Argument; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class GeminiLookupTest. @@ -18,104 +23,233 @@ use Psr\Log\LoggerInterface; */ class GeminiLookupTest extends IslandoraKernelTestBase { - private $geminiLookup; + private $jwtAuth; + + private $logger; + + private $guzzle; private $geminiClient; - private $jwtAuth; + private $mediaSource; - private $logger; + private $entity; + + private $media; /** * {@inheritdoc} */ public function setUp() { parent::setUp(); + + // Mock up dummy objects by default. $prophecy = $this->prophesize(JwtAuth::class); - $prophecy->generateToken()->willReturn("islandora"); $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->findByUri(Argument::any(), Argument::any())->willReturn(NULL); + $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 - * @throws \Drupal\Core\Entity\EntityMalformedException */ 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); - $entity = $prophecy->reveal(); - $this->geminiLookup = new GeminiLookup( - $this->geminiClient, - $this->jwtAuth, - $this->logger + $this->entity = $prophecy->reveal(); + + $gemini_lookup = $this->createGeminiLookup(); + + $this->assertEquals( + NULL, + $gemini_lookup->lookup($this->entity) ); - $this->assertEquals(NULL, $this->geminiLookup->lookup($entity)); } /** * @covers ::lookup * @covers ::__construct - * @throws \Drupal\Core\Entity\EntityMalformedException */ public function testEntityNotFound() { - $prop1 = $this->prophesize(Url::class); - $prop1->toString()->willReturn("http://localhost:8000/node/456"); + $this->mockGeminiClientForFail(); - $prop2 = $this->prophesize(Url::class); - $prop2->setAbsolute()->willReturn($prop1->reveal()); - $url = $prop2->reveal(); + $gemini_lookup = $this->createGeminiLookup(); - $prophecy = $this->prophesize(EntityInterface::class); - $prophecy->id()->willReturn(456); - $prophecy->toUrl()->willReturn($url); - $entity = $prophecy->reveal(); - - $this->geminiLookup = new GeminiLookup( - $this->geminiClient, - $this->jwtAuth, - $this->logger + $this->assertEquals( + NULL, + $gemini_lookup->lookup($this->entity) ); - - $this->assertEquals(NULL, $this->geminiLookup->lookup($entity)); } /** * @covers ::lookup * @covers ::__construct - * @throws \Drupal\Core\Entity\EntityMalformedException */ public function testEntityFound() { - $prop1 = $this->prophesize(Url::class); - $prop1->toString()->willReturn("http://localhost:8000/node/456"); + $this->mockGeminiClientForSuccess(); - $prop2 = $this->prophesize(Url::class); - $prop2->setAbsolute()->willReturn($prop1->reveal()); - $url = $prop2->reveal(); + $gemini_lookup = $this->createGeminiLookup(); - $prophecy = $this->prophesize(EntityInterface::class); - $prophecy->id()->willReturn(456); - $prophecy->toUrl()->willReturn($url); - $entity = $prophecy->reveal(); + $this->assertEquals( + 'http://localhost:8080/fcrepo/rest/abc123', + $gemini_lookup->lookup($this->entity) + ); + } - $prophecy = $this->prophesize(GeminiClient::class); - $prophecy->findByUri(Argument::any(), Argument::any())->willReturn(["http://fedora:8080/some/uri"]); - $this->geminiClient = $prophecy->reveal(); + /** + * @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) + ); + } - $this->geminiLookup = new GeminiLookup( - $this->geminiClient, - $this->jwtAuth, - $this->logger + /** + * @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) ); + } - $this->assertEquals("http://fedora:8080/some/uri", $this->geminiLookup->lookup($entity)); + /** + * @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) + ); } } From 4a7507ca84bcbd19d8c454058c8fd52034603c4a Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Tue, 7 May 2019 13:30:23 -0500 Subject: [PATCH 24/70] Allow multiple view modes (#129) --- islandora.module | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/islandora.module b/islandora.module index 34bbc5fb..6a1f251c 100644 --- a/islandora.module +++ b/islandora.module @@ -374,7 +374,11 @@ function islandora_entity_extra_field_info() { * Implements hook_entity_view(). */ function islandora_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { - if ($view_mode == 'full') { + $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) { From 37f9a366072cdc385213cd42bc0200c3065c0cdd Mon Sep 17 00:00:00 2001 From: dannylamb Date: Fri, 10 May 2019 19:55:45 -0300 Subject: [PATCH 25/70] Adding model for Digital Documents (e.g. pdf) (#130) --- migrate/tags.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/migrate/tags.csv b/migrate/tags.csv index d7766c44..c5054546 100644 --- a/migrate/tags.csv +++ b/migrate/tags.csv @@ -11,3 +11,4 @@ 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 From 8fbf5163dc4265065d801fbc9809ac5f01298030 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Mon, 13 May 2019 12:15:38 -0300 Subject: [PATCH 26/70] Only adding rdf:type to the first entry in the jsonld graph (#131) * Only adding rdf:type to the first entry in the jsonld graph * Whitespace * Searching for entity in graph instead of assuming it's the first --- .../JsonldTypeAlterReaction.php | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php index e130a68e..d34dcba9 100644 --- a/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php +++ b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php @@ -28,30 +28,40 @@ class JsonldTypeAlterReaction extends NormalizerAlterReaction { * {@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(); - // Use a pre-configured field as the source of the additional @type. - if (($entity->hasField($config['source_field'])) && - (!empty($entity->get($config['source_field'])->getValue()))) { - if (isset($normalized['@graph']) && is_array($normalized['@graph'])) { - foreach ($normalized['@graph'] as &$graph) { - 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) { - $graph['@type'][] = $value['uri']; - } + $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'] === $entity->toUrl()->setAbsolute()->toString() . '?_format=jsonld') { + 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 { - $graph['@type'][] = NormalizerBase::escapePrefix($type['value'], $context['namespaces']); - } + } + else { + $elem['@type'][] = NormalizerBase::escapePrefix($type['value'], $context['namespaces']); } } + return; } } } From 6956a85d3c456332cf97dba0cf4d3927811668ef Mon Sep 17 00:00:00 2001 From: dannylamb Date: Tue, 21 May 2019 13:37:33 -0300 Subject: [PATCH 27/70] Respecting jsonld module configuration for ?_format=jsonld (#134) * Respecting jsonld module configuration for ?_format=jsonld * Using 0.2.0 of jsonld --- composer.json | 2 +- .../NormalizerAlterReaction.php | 56 ++++++++++++++++++- .../JsonldTypeAlterReaction.php | 2 +- .../MappingUriPredicateReaction.php | 24 +++++--- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 0429cf69..68cc2c71 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require": { "drupal/context": "^4.0", "drupal/search_api": "~1.8", - "islandora/jsonld": "dev-8.x-1.x", + "islandora/jsonld": "0.2.0", "stomp-php/stomp-php": "4.*", "drupal/jwt": "1.0.0-alpha6", "drupal/filehash": "^1.1", diff --git a/src/ContextReaction/NormalizerAlterReaction.php b/src/ContextReaction/NormalizerAlterReaction.php index 3b7f807b..503c7248 100644 --- a/src/ContextReaction/NormalizerAlterReaction.php +++ b/src/ContextReaction/NormalizerAlterReaction.php @@ -3,7 +3,11 @@ 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 Symfony\Component\DependencyInjection\ContainerInterface; /** * Base class to alter the normalizes Json-ld. @@ -12,10 +16,41 @@ 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; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, + $plugin_id, + $plugin_definition, + ConfigFactoryInterface $config_factory) { + + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->jsonldConfig = $config_factory->get(JsonLdSettingsForm::CONFIG_NAME); + } + + /** + * {@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') + ); + } + + /** + * 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 +61,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) { + $url = $entity->toUrl('canonical', ['absolute' => TRUE]); + if (!$this->jsonldConfig->get(JsonLdSettingsForm::REMOVE_JSONLD_FORMAT)) { + $url->setRouteParameter('_format', 'jsonld'); + } + return $url->toString(); + } + } diff --git a/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php index d34dcba9..56c68cd5 100644 --- a/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php +++ b/src/Plugin/ContextReaction/JsonldTypeAlterReaction.php @@ -43,7 +43,7 @@ class JsonldTypeAlterReaction extends NormalizerAlterReaction { // Search for the entity in the graph. foreach ($normalized['@graph'] as &$elem) { - if ($elem['@id'] === $entity->toUrl()->setAbsolute()->toString() . '?_format=jsonld') { + 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. diff --git a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php index e393f7e6..3fc15a9b 100644 --- a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php +++ b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php @@ -2,9 +2,9 @@ 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; @@ -19,7 +19,7 @@ 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'; @@ -28,8 +28,18 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction implements Con /** * {@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, + MediaSourceService $media_source) { + + parent::__construct( + $configuration, + $plugin_id, + $plugin_definition, + $config_factory + ); $this->mediaSource = $media_source; } @@ -41,6 +51,7 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction implements Con $configuration, $plugin_id, $plugin_definition, + $container->get('config.factory'), $container->get('islandora.media_source_service') ); } @@ -59,10 +70,7 @@ 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']); } From 845efd3520de92be9ded9e58f8be66c3febd66af Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 23 May 2019 15:55:06 -0300 Subject: [PATCH 28/70] Simpler upload form to link media and node in one shot (#135) --- composer.json | 1 + islandora.info.yml | 1 + .../webform.webform.islandora_upload.yml | 178 +++++++++++++++++ .../IslandoraUploadWebformHandler.php | 183 ++++++++++++++++++ tests/src/Kernel/IslandoraKernelTestBase.php | 5 + .../IslandoraUploadWebformHandlerTest.php | 158 +++++++++++++++ 6 files changed, 526 insertions(+) create mode 100644 modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml create mode 100644 src/Plugin/WebformHandler/IslandoraUploadWebformHandler.php create mode 100644 tests/src/Kernel/IslandoraUploadWebformHandlerTest.php diff --git a/composer.json b/composer.json index 68cc2c71..478474aa 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", + "drupal/webform" : "^5.2", "islandora/crayfish-commons": "^0.0" }, "require-dev": { diff --git a/islandora.info.yml b/islandora.info.yml index 0e0a265e..fc0dd9f1 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -31,3 +31,4 @@ dependencies: - content_translation - flysystem - token + - webform diff --git a/modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml b/modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml new file mode 100644 index 00000000..76a7687a --- /dev/null +++ b/modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml @@ -0,0 +1,178 @@ +langcode: en +status: open +dependencies: + module: + - islandora +open: null +close: null +weight: 0 +uid: 1 +template: false +archive: false +id: islandora_upload +title: 'Islandora Upload' +description: '' +category: '' +elements: "content_type:\n '#type': webform_entity_select\n '#title': 'Content Type'\n '#required': true\n '#target_type': node_type\n '#selection_handler': 'default:node_type'\nmodel:\n '#type': webform_term_select\n '#title': Model\n '#required': true\n '#vocabulary': islandora_models\n '#breadcrumb_delimiter': ''\nmedia_type:\n '#type': webform_entity_select\n '#title': 'Media Type'\n '#required': true\n '#target_type': media_type\n '#selection_handler': 'default:media_type'\nmedia_use:\n '#type': webform_term_select\n '#title': 'Media Use'\n '#required': true\n '#vocabulary': islandora_media_use\n '#breadcrumb_delimiter': ''\nfile:\n '#type': managed_file\n '#title': File\n '#required': true\n '#uri_scheme': fedora\n '#file_extensions': 'gif jpg jpeg png bmp eps tif pict psd txt rtf html odf pdf doc docx ppt pptx xls xlsx xml avi mov mp3 ogg wav bz2 dmg gz jar rar sit svg tar zip'" +css: '' +javascript: '' +settings: + ajax: false + ajax_scroll_top: form + page: true + page_submit_path: '' + page_confirm_path: '' + form_title: both + form_submit_once: false + form_exception_message: '' + form_open_message: '' + form_close_message: '' + form_previous_submissions: true + form_confidential: false + form_confidential_message: '' + form_remote_addr: true + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: '' + form_reset: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_autofocus: false + form_details_toggle: false + form_access_denied: default + form_access_denied_title: '' + form_access_denied_message: '' + form_access_denied_attributes: { } + form_file_limit: '' + submission_label: '' + submission_log: false + submission_views: { } + submission_views_replace: { } + submission_user_columns: { } + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: '' + submission_access_denied_message: '' + submission_access_denied_attributes: { } + submission_exception_message: '' + submission_locked_message: '' + submission_excluded_elements: { } + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + previous_submission_message: '' + previous_submissions_message: '' + autofill: false + autofill_message: '' + autofill_excluded_elements: { } + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_start_label: '' + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: '' + wizard_track: '' + preview: 0 + preview_label: '' + preview_title: '' + preview_message: '' + preview_attributes: { } + preview_excluded_elements: { } + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: '' + draft_loaded_message: '' + confirmation_type: page + confirmation_title: '' + confirmation_message: '' + confirmation_url: '' + confirmation_attributes: { } + confirmation_back: true + confirmation_back_label: '' + confirmation_back_attributes: { } + confirmation_exclude_query: false + confirmation_exclude_token: false + limit_total: null + limit_total_interval: null + limit_total_message: '' + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: '' + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: none + purge_days: null + results_disabled: false + results_disabled_ignore: false + token_update: false +access: + create: + roles: + - anonymous + - authenticated + users: { } + permissions: { } + view_any: + roles: { } + users: { } + permissions: { } + update_any: + roles: { } + users: { } + permissions: { } + delete_any: + roles: { } + users: { } + permissions: { } + purge_any: + roles: { } + users: { } + permissions: { } + view_own: + roles: { } + users: { } + permissions: { } + update_own: + roles: { } + users: { } + permissions: { } + delete_own: + roles: { } + users: { } + permissions: { } + administer: + roles: { } + users: { } + permissions: { } + test: + roles: { } + users: { } + permissions: { } + configuration: + roles: { } + users: { } + permissions: { } +handlers: + create_a_node_media_combo_: + id: islandora_upload + label: 'Create a node/media combo.' + handler_id: create_a_node_media_combo_ + status: true + conditions: { } + weight: 0 + settings: { } diff --git a/src/Plugin/WebformHandler/IslandoraUploadWebformHandler.php b/src/Plugin/WebformHandler/IslandoraUploadWebformHandler.php new file mode 100644 index 00000000..2f5ba26c --- /dev/null +++ b/src/Plugin/WebformHandler/IslandoraUploadWebformHandler.php @@ -0,0 +1,183 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_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('logger.factory'), + $container->get('config.factory'), + $container->get('entity_type.manager'), + $container->get('webform_submission.conditions_validator'), + $container->get('entity_field.manager'), + $container->get('islandora.media_source_service') + ); + } + + /** + * {@inheritdoc} + */ + public function alterForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { + // Strip out content types that don't have the required fields. + foreach ($form['elements']['content_type']['#options'] as $k => $v) { + if (!$this->bundleHasField('node', $k, 'field_model')) { + unset($form['elements']['content_type']['#options'][$k]); + } + } + + // Strip out media types that don't have the required fields. + foreach ($form['elements']['media_type']['#options'] as $k => $v) { + if (!$this->bundleHasField('media', $k, 'field_media_of') || + !$this->bundleHasField('media', $k, 'field_media_use')) { + unset($form['elements']['media_type']['#options'][$k]); + } + } + + } + + /** + * Utility function for filtering out bundles without the required fields. + * + * @param string $entity_type + * Entity type. + * @param string $bundle + * Bundle. + * @param string $field_name + * Field name. + * + * @return bool + * TRUE if the bundle has the specified field. + */ + protected function bundleHasField($entity_type, $bundle, $field_name) { + $all_bundle_fields = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle); + return isset($all_bundle_fields[$field_name]); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { + // Get form field values. + $submission_array = $webform_submission->getData(); + $file = $this->entityTypeManager->getStorage('file')->load($submission_array['file']); + $model = $this->entityTypeManager->getStorage('taxonomy_term')->load($submission_array['model']); + $use = $this->entityTypeManager->getStorage('taxonomy_term')->load($submission_array['media_use']); + + // Make the node. + $this->node = $this->entityTypeManager->getStorage('node')->create([ + 'type' => $submission_array['content_type'], + 'status' => TRUE, + 'title' => $file->getFileName(), + 'field_model' => [$model], + ]); + $this->node->save(); + + // Make the media. + $source_field = $this->mediaSource->getSourceFieldName($submission_array['media_type']); + $media = $this->entityTypeManager->getStorage('media')->create([ + 'bundle' => $submission_array['media_type'], + 'status' => TRUE, + 'name' => $file->getFileName(), + 'field_media_use' => [$use], + 'field_media_of' => [$this->node], + $source_field => [$file], + ]); + $media->save(); + } + + /** + * {@inheritdoc} + */ + public function confirmForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { + // Redirect the user to the node's edit form. + $form_state->setRedirect('entity.node.edit_form', ['node' => $this->node->id()]); + } + +} diff --git a/tests/src/Kernel/IslandoraKernelTestBase.php b/tests/src/Kernel/IslandoraKernelTestBase.php index 5690bc1f..71a435c0 100644 --- a/tests/src/Kernel/IslandoraKernelTestBase.php +++ b/tests/src/Kernel/IslandoraKernelTestBase.php @@ -34,8 +34,10 @@ abstract class IslandoraKernelTestBase extends KernelTestBase { 'key', 'jwt', 'file', + 'taxonomy', 'image', 'media', + 'webform', 'islandora', 'flysystem', ]; @@ -49,10 +51,13 @@ abstract class IslandoraKernelTestBase extends KernelTestBase { // Bootstrap minimal Drupal environment to run the tests. $this->installSchema('system', 'sequences'); $this->installSchema('node', 'node_access'); + $this->installSchema('file', 'file_usage'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); $this->installEntitySchema('context'); $this->installEntitySchema('file'); + $this->installEntitySchema('media'); + $this->installEntitySchema('taxonomy_term'); $this->installConfig('filter'); } diff --git a/tests/src/Kernel/IslandoraUploadWebformHandlerTest.php b/tests/src/Kernel/IslandoraUploadWebformHandlerTest.php new file mode 100644 index 00000000..bec1ee70 --- /dev/null +++ b/tests/src/Kernel/IslandoraUploadWebformHandlerTest.php @@ -0,0 +1,158 @@ +testType = $this->container->get('entity_type.manager')->getStorage('node_type')->create([ + 'type' => 'test_type', + 'name' => 'Test Type', + ]); + $this->testType->save(); + $this->createEntityReferenceField('node', 'test_type', 'field_member_of', 'Member Of', 'node', 'default', [], 2); + $this->createEntityReferenceField('node', 'test_type', 'field_model', 'Model', 'taxonomy_term', 'default', [], 2); + + // Create a media type. + $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_media_use', 'Media Use', 'taxonomy_term', 'default', [], 2); + + // Create a vocabulary. + $this->testVocabulary = $this->container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')->create([ + 'name' => 'Test Vocabulary', + 'vid' => 'test_vocabulary', + ]); + $this->testVocabulary->save(); + + // Create two terms. + $this->modelTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ + 'name' => 'Binary', + 'vid' => $this->testVocabulary->id(), + ]); + $this->modelTerm->save(); + + $this->useTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ + 'name' => 'Original File', + 'vid' => $this->testVocabulary->id(), + ]); + $this->useTerm->save(); + + // Pretend this is the user uploaded file. + $this->file = $this->container->get('entity_type.manager')->getStorage('file')->create([ + 'uri' => "public://test_file.txt", + 'filename' => "test_file.txt", + 'filemime' => "text/plain", + 'status' => FILE_STATUS_PERMANENT, + ]); + $this->file->save(); + } + + /** + * @covers \Drupal\islandora\Plugin\WebformHandler\IslandoraUploadWebformHandler::submitForm + * @covers \Drupal\islandora\Plugin\WebformHandler\IslandoraUploadWebformHandler::confirmForm + */ + public function testSubmitForm() { + $handler_manager = $this->container->get('plugin.manager.webform.handler'); + $handler = $handler_manager->createInstance('islandora_upload'); + + $form = []; + + $prophecy = $this->prophesize(FormStateInterface::class); + $prophecy->setRedirect('entity.node.edit_form', ['node' => 1])->shouldBeCalled(); + $form_state = $prophecy->reveal(); + + $prophecy = $this->prophesize(WebformSubmissionInterface::class); + $prophecy->getData()->willReturn([ + 'content_type' => 'test_type', + 'media_type' => 'test_media_type', + 'model' => "{$this->modelTerm->id()}", + 'media_use' => "{$this->useTerm->id()}", + 'file' => $this->file->id(), + ]); + $webform_submission = $prophecy->reveal(); + + $handler->submitForm($form, $form_state, $webform_submission); + + $node = $this->container->get('entity_type.manager')->getStorage('node')->load(1); + $media = $this->container->get('entity_type.manager')->getStorage('media')->load(1); + + // Assert content type. + $actual = $node->bundle(); + $expected = $this->testType->id(); + $this->assertTrue($actual == $expected, "Incorrect Content Type: Expected $expected, received $actual"); + + // Assert model. + $actual = $node->get('field_model')->referencedEntities()[0]->id(); + $expected = $this->modelTerm->id(); + $this->assertTrue($actual == $expected, "Incorrect Model: Expected $expected, received $actual"); + + // Assert title. + $actual = $node->getTitle(); + $expected = $this->file->getFileName(); + $this->assertTrue($actual == $expected, "Incorrect Title: Expected $expected, received $actual"); + + // Assert media type. + $actual = $media->bundle(); + $expected = $this->testMediaType->id(); + $this->assertTrue($actual == $expected, "Incorrect Media Type: Expected $expected, received $actual"); + + // Assert media use. + $actual = $media->get('field_media_use')->referencedEntities()[0]->id(); + $expected = $this->useTerm->id(); + $this->assertTrue($actual == $expected, "Incorrect Media Use: Expected $expected, received $actual"); + + // Assert media name. + $actual = $media->getName(); + $expected = $this->file->getFileName(); + $this->assertTrue($actual == $expected, "Incorrect Name: Expected $expected, received $actual"); + + // Assert media is using file. + $actual = $media->get('field_media_file')->referencedEntities()[0]->id(); + $expected = $this->file->id(); + $this->assertTrue($actual == $expected, "Incorrect File: Expected $expected, received $actual"); + + // Assert media is linked to node. + $actual = $media->get('field_media_of')->referencedEntities()[0]->id(); + $expected = $node->id(); + $this->assertTrue($actual == $expected, "Linked to incorrect node: Expected $expected, received $actual"); + + // Assert that setRedirect is called in the confirm form function. + $handler->confirmForm($form, $form_state, $webform_submission); + } + +} From ea2236ab3139b72e70ece1b0dd5fb8a3e4e20ed5 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 23 May 2019 16:30:48 -0300 Subject: [PATCH 29/70] Forcing flysystem downloads to be language neutral (#137) * Forcing flysystem downloads to be language neutral * Coder.... --- src/Flysystem/Fedora.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Flysystem/Fedora.php b/src/Flysystem/Fedora.php index fadd8a0f..fd347b89 100644 --- a/src/Flysystem/Fedora.php +++ b/src/Flysystem/Fedora.php @@ -4,6 +4,7 @@ namespace Drupal\islandora\Flysystem; 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; @@ -116,4 +117,24 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac 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 = \Drupal::languageManager()->getLanguage('und'); + return Url::fromRoute( + 'flysystem.serve', + $arguments, + ['absolute' => TRUE, 'language' => $undefined] + )->toString(); + } + } From 8e02b05b7a04e3a4ccf831a234e60131ae25db89 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 23 May 2019 17:56:24 -0300 Subject: [PATCH 30/70] Swapping file and media urls in jsonld (#136) --- .../MappingUriPredicateReaction.php | 3 +- .../MappingUriPredicateReactionTest.php | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php index 3fc15a9b..a5ad5880 100644 --- a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php +++ b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php @@ -77,9 +77,10 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction { 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'] = $file->url('canonical', ['absolute' => TRUE]); } if (isset($graph[$drupal_predicate])) { if (!is_array($graph[$drupal_predicate])) { diff --git a/tests/src/Functional/MappingUriPredicateReactionTest.php b/tests/src/Functional/MappingUriPredicateReactionTest.php index 31d27bd5..45ca762d 100644 --- a/tests/src/Functional/MappingUriPredicateReactionTest.php +++ b/tests/src/Functional/MappingUriPredicateReactionTest.php @@ -134,4 +134,62 @@ class MappingUriPredicateReactionTest extends IslandoraFunctionalTestBase { ); } + /** + * @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]['@value'], + 'Missing alter added predicate.' + ); + $this->assertEquals( + $file_url, + $json['@graph'][0]['@id'], + 'Alter did not swap "@id" of media with file url.' + ); + + } + } From b250b01a4e18f03efbd7fde6bb11d150012b3397 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Tue, 28 May 2019 18:27:39 -0300 Subject: [PATCH 31/70] Revert "Simpler upload form to link media and node in one shot" (#139) * Revert "Simpler upload form to link media and node in one shot (#135)" This reverts commit 845efd3520de92be9ded9e58f8be66c3febd66af. * Coder --- composer.json | 1 - islandora.info.yml | 1 - .../webform.webform.islandora_upload.yml | 178 ----------------- src/EventGenerator/EventGenerator.php | 1 - src/MediaSource/MediaSourceService.php | 4 +- .../IslandoraUploadWebformHandler.php | 183 ------------------ tests/src/Kernel/IslandoraKernelTestBase.php | 5 - .../IslandoraUploadWebformHandlerTest.php | 158 --------------- 8 files changed, 2 insertions(+), 529 deletions(-) delete mode 100644 modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml delete mode 100644 src/Plugin/WebformHandler/IslandoraUploadWebformHandler.php delete mode 100644 tests/src/Kernel/IslandoraUploadWebformHandlerTest.php diff --git a/composer.json b/composer.json index 478474aa..68cc2c71 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,6 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "drupal/webform" : "^5.2", "islandora/crayfish-commons": "^0.0" }, "require-dev": { diff --git a/islandora.info.yml b/islandora.info.yml index fc0dd9f1..0e0a265e 100644 --- a/islandora.info.yml +++ b/islandora.info.yml @@ -31,4 +31,3 @@ dependencies: - content_translation - flysystem - token - - webform diff --git a/modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml b/modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml deleted file mode 100644 index 76a7687a..00000000 --- a/modules/islandora_core_feature/config/install/webform.webform.islandora_upload.yml +++ /dev/null @@ -1,178 +0,0 @@ -langcode: en -status: open -dependencies: - module: - - islandora -open: null -close: null -weight: 0 -uid: 1 -template: false -archive: false -id: islandora_upload -title: 'Islandora Upload' -description: '' -category: '' -elements: "content_type:\n '#type': webform_entity_select\n '#title': 'Content Type'\n '#required': true\n '#target_type': node_type\n '#selection_handler': 'default:node_type'\nmodel:\n '#type': webform_term_select\n '#title': Model\n '#required': true\n '#vocabulary': islandora_models\n '#breadcrumb_delimiter': ''\nmedia_type:\n '#type': webform_entity_select\n '#title': 'Media Type'\n '#required': true\n '#target_type': media_type\n '#selection_handler': 'default:media_type'\nmedia_use:\n '#type': webform_term_select\n '#title': 'Media Use'\n '#required': true\n '#vocabulary': islandora_media_use\n '#breadcrumb_delimiter': ''\nfile:\n '#type': managed_file\n '#title': File\n '#required': true\n '#uri_scheme': fedora\n '#file_extensions': 'gif jpg jpeg png bmp eps tif pict psd txt rtf html odf pdf doc docx ppt pptx xls xlsx xml avi mov mp3 ogg wav bz2 dmg gz jar rar sit svg tar zip'" -css: '' -javascript: '' -settings: - ajax: false - ajax_scroll_top: form - page: true - page_submit_path: '' - page_confirm_path: '' - form_title: both - form_submit_once: false - form_exception_message: '' - form_open_message: '' - form_close_message: '' - form_previous_submissions: true - form_confidential: false - form_confidential_message: '' - form_remote_addr: true - form_convert_anonymous: false - form_prepopulate: false - form_prepopulate_source_entity: false - form_prepopulate_source_entity_required: false - form_prepopulate_source_entity_type: '' - form_reset: false - form_disable_autocomplete: false - form_novalidate: false - form_disable_inline_errors: false - form_required: false - form_unsaved: false - form_disable_back: false - form_submit_back: false - form_autofocus: false - form_details_toggle: false - form_access_denied: default - form_access_denied_title: '' - form_access_denied_message: '' - form_access_denied_attributes: { } - form_file_limit: '' - submission_label: '' - submission_log: false - submission_views: { } - submission_views_replace: { } - submission_user_columns: { } - submission_user_duplicate: false - submission_access_denied: default - submission_access_denied_title: '' - submission_access_denied_message: '' - submission_access_denied_attributes: { } - submission_exception_message: '' - submission_locked_message: '' - submission_excluded_elements: { } - submission_exclude_empty: false - submission_exclude_empty_checkbox: false - previous_submission_message: '' - previous_submissions_message: '' - autofill: false - autofill_message: '' - autofill_excluded_elements: { } - wizard_progress_bar: true - wizard_progress_pages: false - wizard_progress_percentage: false - wizard_progress_link: false - wizard_start_label: '' - wizard_preview_link: false - wizard_confirmation: true - wizard_confirmation_label: '' - wizard_track: '' - preview: 0 - preview_label: '' - preview_title: '' - preview_message: '' - preview_attributes: { } - preview_excluded_elements: { } - preview_exclude_empty: true - preview_exclude_empty_checkbox: false - draft: none - draft_multiple: false - draft_auto_save: false - draft_saved_message: '' - draft_loaded_message: '' - confirmation_type: page - confirmation_title: '' - confirmation_message: '' - confirmation_url: '' - confirmation_attributes: { } - confirmation_back: true - confirmation_back_label: '' - confirmation_back_attributes: { } - confirmation_exclude_query: false - confirmation_exclude_token: false - limit_total: null - limit_total_interval: null - limit_total_message: '' - limit_total_unique: false - limit_user: null - limit_user_interval: null - limit_user_message: '' - limit_user_unique: false - entity_limit_total: null - entity_limit_total_interval: null - entity_limit_user: null - entity_limit_user_interval: null - purge: none - purge_days: null - results_disabled: false - results_disabled_ignore: false - token_update: false -access: - create: - roles: - - anonymous - - authenticated - users: { } - permissions: { } - view_any: - roles: { } - users: { } - permissions: { } - update_any: - roles: { } - users: { } - permissions: { } - delete_any: - roles: { } - users: { } - permissions: { } - purge_any: - roles: { } - users: { } - permissions: { } - view_own: - roles: { } - users: { } - permissions: { } - update_own: - roles: { } - users: { } - permissions: { } - delete_own: - roles: { } - users: { } - permissions: { } - administer: - roles: { } - users: { } - permissions: { } - test: - roles: { } - users: { } - permissions: { } - configuration: - roles: { } - users: { } - permissions: { } -handlers: - create_a_node_media_combo_: - id: islandora_upload - label: 'Create a node/media combo.' - handler_id: create_a_node_media_combo_ - status: true - conditions: { } - weight: 0 - settings: { } diff --git a/src/EventGenerator/EventGenerator.php b/src/EventGenerator/EventGenerator.php index edc32183..df258b6b 100644 --- a/src/EventGenerator/EventGenerator.php +++ b/src/EventGenerator/EventGenerator.php @@ -3,7 +3,6 @@ namespace Drupal\islandora\EventGenerator; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Url; use Drupal\file\FileInterface; use Drupal\user\UserInterface; diff --git a/src/MediaSource/MediaSourceService.php b/src/MediaSource/MediaSourceService.php index af23c26a..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, @@ -242,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/WebformHandler/IslandoraUploadWebformHandler.php b/src/Plugin/WebformHandler/IslandoraUploadWebformHandler.php deleted file mode 100644 index 2f5ba26c..00000000 --- a/src/Plugin/WebformHandler/IslandoraUploadWebformHandler.php +++ /dev/null @@ -1,183 +0,0 @@ -entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_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('logger.factory'), - $container->get('config.factory'), - $container->get('entity_type.manager'), - $container->get('webform_submission.conditions_validator'), - $container->get('entity_field.manager'), - $container->get('islandora.media_source_service') - ); - } - - /** - * {@inheritdoc} - */ - public function alterForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { - // Strip out content types that don't have the required fields. - foreach ($form['elements']['content_type']['#options'] as $k => $v) { - if (!$this->bundleHasField('node', $k, 'field_model')) { - unset($form['elements']['content_type']['#options'][$k]); - } - } - - // Strip out media types that don't have the required fields. - foreach ($form['elements']['media_type']['#options'] as $k => $v) { - if (!$this->bundleHasField('media', $k, 'field_media_of') || - !$this->bundleHasField('media', $k, 'field_media_use')) { - unset($form['elements']['media_type']['#options'][$k]); - } - } - - } - - /** - * Utility function for filtering out bundles without the required fields. - * - * @param string $entity_type - * Entity type. - * @param string $bundle - * Bundle. - * @param string $field_name - * Field name. - * - * @return bool - * TRUE if the bundle has the specified field. - */ - protected function bundleHasField($entity_type, $bundle, $field_name) { - $all_bundle_fields = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle); - return isset($all_bundle_fields[$field_name]); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { - // Get form field values. - $submission_array = $webform_submission->getData(); - $file = $this->entityTypeManager->getStorage('file')->load($submission_array['file']); - $model = $this->entityTypeManager->getStorage('taxonomy_term')->load($submission_array['model']); - $use = $this->entityTypeManager->getStorage('taxonomy_term')->load($submission_array['media_use']); - - // Make the node. - $this->node = $this->entityTypeManager->getStorage('node')->create([ - 'type' => $submission_array['content_type'], - 'status' => TRUE, - 'title' => $file->getFileName(), - 'field_model' => [$model], - ]); - $this->node->save(); - - // Make the media. - $source_field = $this->mediaSource->getSourceFieldName($submission_array['media_type']); - $media = $this->entityTypeManager->getStorage('media')->create([ - 'bundle' => $submission_array['media_type'], - 'status' => TRUE, - 'name' => $file->getFileName(), - 'field_media_use' => [$use], - 'field_media_of' => [$this->node], - $source_field => [$file], - ]); - $media->save(); - } - - /** - * {@inheritdoc} - */ - public function confirmForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) { - // Redirect the user to the node's edit form. - $form_state->setRedirect('entity.node.edit_form', ['node' => $this->node->id()]); - } - -} diff --git a/tests/src/Kernel/IslandoraKernelTestBase.php b/tests/src/Kernel/IslandoraKernelTestBase.php index 71a435c0..5690bc1f 100644 --- a/tests/src/Kernel/IslandoraKernelTestBase.php +++ b/tests/src/Kernel/IslandoraKernelTestBase.php @@ -34,10 +34,8 @@ abstract class IslandoraKernelTestBase extends KernelTestBase { 'key', 'jwt', 'file', - 'taxonomy', 'image', 'media', - 'webform', 'islandora', 'flysystem', ]; @@ -51,13 +49,10 @@ abstract class IslandoraKernelTestBase extends KernelTestBase { // Bootstrap minimal Drupal environment to run the tests. $this->installSchema('system', 'sequences'); $this->installSchema('node', 'node_access'); - $this->installSchema('file', 'file_usage'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); $this->installEntitySchema('context'); $this->installEntitySchema('file'); - $this->installEntitySchema('media'); - $this->installEntitySchema('taxonomy_term'); $this->installConfig('filter'); } diff --git a/tests/src/Kernel/IslandoraUploadWebformHandlerTest.php b/tests/src/Kernel/IslandoraUploadWebformHandlerTest.php deleted file mode 100644 index bec1ee70..00000000 --- a/tests/src/Kernel/IslandoraUploadWebformHandlerTest.php +++ /dev/null @@ -1,158 +0,0 @@ -testType = $this->container->get('entity_type.manager')->getStorage('node_type')->create([ - 'type' => 'test_type', - 'name' => 'Test Type', - ]); - $this->testType->save(); - $this->createEntityReferenceField('node', 'test_type', 'field_member_of', 'Member Of', 'node', 'default', [], 2); - $this->createEntityReferenceField('node', 'test_type', 'field_model', 'Model', 'taxonomy_term', 'default', [], 2); - - // Create a media type. - $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_media_use', 'Media Use', 'taxonomy_term', 'default', [], 2); - - // Create a vocabulary. - $this->testVocabulary = $this->container->get('entity_type.manager')->getStorage('taxonomy_vocabulary')->create([ - 'name' => 'Test Vocabulary', - 'vid' => 'test_vocabulary', - ]); - $this->testVocabulary->save(); - - // Create two terms. - $this->modelTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ - 'name' => 'Binary', - 'vid' => $this->testVocabulary->id(), - ]); - $this->modelTerm->save(); - - $this->useTerm = $this->container->get('entity_type.manager')->getStorage('taxonomy_term')->create([ - 'name' => 'Original File', - 'vid' => $this->testVocabulary->id(), - ]); - $this->useTerm->save(); - - // Pretend this is the user uploaded file. - $this->file = $this->container->get('entity_type.manager')->getStorage('file')->create([ - 'uri' => "public://test_file.txt", - 'filename' => "test_file.txt", - 'filemime' => "text/plain", - 'status' => FILE_STATUS_PERMANENT, - ]); - $this->file->save(); - } - - /** - * @covers \Drupal\islandora\Plugin\WebformHandler\IslandoraUploadWebformHandler::submitForm - * @covers \Drupal\islandora\Plugin\WebformHandler\IslandoraUploadWebformHandler::confirmForm - */ - public function testSubmitForm() { - $handler_manager = $this->container->get('plugin.manager.webform.handler'); - $handler = $handler_manager->createInstance('islandora_upload'); - - $form = []; - - $prophecy = $this->prophesize(FormStateInterface::class); - $prophecy->setRedirect('entity.node.edit_form', ['node' => 1])->shouldBeCalled(); - $form_state = $prophecy->reveal(); - - $prophecy = $this->prophesize(WebformSubmissionInterface::class); - $prophecy->getData()->willReturn([ - 'content_type' => 'test_type', - 'media_type' => 'test_media_type', - 'model' => "{$this->modelTerm->id()}", - 'media_use' => "{$this->useTerm->id()}", - 'file' => $this->file->id(), - ]); - $webform_submission = $prophecy->reveal(); - - $handler->submitForm($form, $form_state, $webform_submission); - - $node = $this->container->get('entity_type.manager')->getStorage('node')->load(1); - $media = $this->container->get('entity_type.manager')->getStorage('media')->load(1); - - // Assert content type. - $actual = $node->bundle(); - $expected = $this->testType->id(); - $this->assertTrue($actual == $expected, "Incorrect Content Type: Expected $expected, received $actual"); - - // Assert model. - $actual = $node->get('field_model')->referencedEntities()[0]->id(); - $expected = $this->modelTerm->id(); - $this->assertTrue($actual == $expected, "Incorrect Model: Expected $expected, received $actual"); - - // Assert title. - $actual = $node->getTitle(); - $expected = $this->file->getFileName(); - $this->assertTrue($actual == $expected, "Incorrect Title: Expected $expected, received $actual"); - - // Assert media type. - $actual = $media->bundle(); - $expected = $this->testMediaType->id(); - $this->assertTrue($actual == $expected, "Incorrect Media Type: Expected $expected, received $actual"); - - // Assert media use. - $actual = $media->get('field_media_use')->referencedEntities()[0]->id(); - $expected = $this->useTerm->id(); - $this->assertTrue($actual == $expected, "Incorrect Media Use: Expected $expected, received $actual"); - - // Assert media name. - $actual = $media->getName(); - $expected = $this->file->getFileName(); - $this->assertTrue($actual == $expected, "Incorrect Name: Expected $expected, received $actual"); - - // Assert media is using file. - $actual = $media->get('field_media_file')->referencedEntities()[0]->id(); - $expected = $this->file->id(); - $this->assertTrue($actual == $expected, "Incorrect File: Expected $expected, received $actual"); - - // Assert media is linked to node. - $actual = $media->get('field_media_of')->referencedEntities()[0]->id(); - $expected = $node->id(); - $this->assertTrue($actual == $expected, "Linked to incorrect node: Expected $expected, received $actual"); - - // Assert that setRedirect is called in the confirm form function. - $handler->confirmForm($form, $form_state, $webform_submission); - } - -} From a6c89adbbe5b09b2f22a39c12ca27d8da1b78240 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Tue, 28 May 2019 20:41:22 -0500 Subject: [PATCH 32/70] Set default settings (#138) --- config/install/islandora.settings.yml | 4 ++++ islandora.module | 2 +- src/Form/IslandoraSettingsForm.php | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 config/install/islandora.settings.yml 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/islandora.module b/islandora.module index 6a1f251c..8b0d52e1 100644 --- a/islandora.module +++ b/islandora.module @@ -356,7 +356,7 @@ 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) ? $config_factory->get(IslandoraSettingsForm::GEMINI_PSEUDO) : []; + $pseudo_bundles = $config_factory->get(IslandoraSettingsForm::GEMINI_PSEUDO); foreach ($pseudo_bundles as $key) { list($bundle, $content_entity) = explode(":", $key); diff --git a/src/Form/IslandoraSettingsForm.php b/src/Form/IslandoraSettingsForm.php index 6ec80165..8079279d 100644 --- a/src/Form/IslandoraSettingsForm.php +++ b/src/Form/IslandoraSettingsForm.php @@ -80,22 +80,22 @@ 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) ? $config->get(self::JWT_EXPIRY) : '+2 hour', + '#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) ? $config->get(self::GEMINI_URL) : '', + '#default_value' => $config->get(self::GEMINI_URL), ]; - $selected_bundles = $config->get(self::GEMINI_PSEUDO) ? $config->get(self::GEMINI_PSEUDO) : []; + $selected_bundles = $config->get(self::GEMINI_PSEUDO); $options = []; foreach (['node', 'media', 'taxonomy_term'] as $content_entity) { From ba806fd719f19af90fdcaa458de25fa461ceb4f5 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Wed, 29 May 2019 10:57:13 -0500 Subject: [PATCH 33/70] Switch @value to @id (#140) --- src/Plugin/ContextReaction/MappingUriPredicateReaction.php | 4 ++-- tests/src/Functional/MappingUriPredicateReactionTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php index a5ad5880..c59e48c8 100644 --- a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php +++ b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php @@ -91,7 +91,7 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction { $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; } @@ -99,7 +99,7 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction { else { $graph[$drupal_predicate] = []; } - $graph[$drupal_predicate][] = ["@value" => $url]; + $graph[$drupal_predicate][] = ["@id" => $url]; return; } } diff --git a/tests/src/Functional/MappingUriPredicateReactionTest.php b/tests/src/Functional/MappingUriPredicateReactionTest.php index 45ca762d..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,7 +129,7 @@ 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.' ); } @@ -181,7 +181,7 @@ class MappingUriPredicateReactionTest extends IslandoraFunctionalTestBase { $json = \GuzzleHttp\json_decode($new_contents, TRUE); $this->assertEquals( "$media_url?_format=jsonld", - $json['@graph'][0]['http://www.iana.org/assignments/relation/describedby'][0]['@value'], + $json['@graph'][0]['http://www.iana.org/assignments/relation/describedby'][0]['@id'], 'Missing alter added predicate.' ); $this->assertEquals( From 578ab47fd98d80dd8a9dbbd9b65eb78e82797dd1 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Wed, 29 May 2019 14:55:31 -0300 Subject: [PATCH 34/70] Forcing urls in event messages to be language neutral as well (#141) --- islandora.services.yml | 1 + src/EventGenerator/EventGenerator.php | 30 ++++++++++++++++++-- src/Flysystem/Fedora.php | 19 +++++++++++-- tests/src/Kernel/EventGeneratorTest.php | 2 +- tests/src/Kernel/FedoraPluginTest.php | 4 ++- tests/src/Kernel/IslandoraKernelTestBase.php | 1 + 6 files changed, 49 insertions(+), 8 deletions(-) diff --git a/islandora.services.yml b/islandora.services.yml index 0ba96794..fb7026bf 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -3,6 +3,7 @@ services: islandora.eventgenerator: class: Drupal\islandora\EventGenerator\EventGenerator + arguments: ['@language_manager'] islandora.stomp: class: Stomp\StatefulStomp factory: ['Drupal\islandora\StompFactory', create] diff --git a/src/EventGenerator/EventGenerator.php b/src/EventGenerator/EventGenerator.php index df258b6b..ac258163 100644 --- a/src/EventGenerator/EventGenerator.php +++ b/src/EventGenerator/EventGenerator.php @@ -3,7 +3,8 @@ namespace Drupal\islandora\EventGenerator; use Drupal\Core\Entity\EntityInterface; -use Drupal\file\FileInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Url; use Drupal\user\UserInterface; /** @@ -13,19 +14,42 @@ use Drupal\user\UserInterface; */ class EventGenerator implements EventGeneratorInterface { + /** + * Language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Constructor. + * + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * Language manager. + */ + public function __construct(LanguageManagerInterface $language_manager) { + $this->languageManager = $language_manager; + } + /** * {@inheritdoc} */ public function generateEvent(EntityInterface $entity, UserInterface $user, array $data) { $user_url = $user->toUrl()->setAbsolute()->toString(); + $entity_type = $entity->getEntityTypeId(); - if ($entity instanceof FileInterface) { + if ($entity_type == 'file') { $entity_url = $entity->url(); $mimetype = $entity->getMimeType(); } else { - $entity_url = $entity->toUrl()->setAbsolute()->toString(); + $undefined = $this->languageManager->getLanguage('und'); + $entity_url = Url::fromRoute( + "rest.entity.$entity_type.GET", + [$entity_type => $entity->id()], + ['absolute' => TRUE, 'language' => $undefined] + )->toString(); $mimetype = 'text/html'; } diff --git a/src/Flysystem/Fedora.php b/src/Flysystem/Fedora.php index fd347b89..85f42b1a 100644 --- a/src/Flysystem/Fedora.php +++ b/src/Flysystem/Fedora.php @@ -2,6 +2,7 @@ namespace Drupal\islandora\Flysystem; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Url; @@ -30,6 +31,13 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac protected $mimeTypeGuesser; + /** + * Language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + /** * Constructs a Fedora plugin for Flysystem. * @@ -37,13 +45,17 @@ 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; } /** @@ -63,7 +75,8 @@ 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') ); } @@ -129,7 +142,7 @@ class Fedora implements FlysystemPluginInterface, ContainerFactoryPluginInterfac ]; // Force file urls to be language neutral. - $undefined = \Drupal::languageManager()->getLanguage('und'); + $undefined = $this->languageManager->getLanguage('und'); return Url::fromRoute( 'flysystem.serve', $arguments, diff --git a/tests/src/Kernel/EventGeneratorTest.php b/tests/src/Kernel/EventGeneratorTest.php index 9f0bed06..c5703f3f 100644 --- a/tests/src/Kernel/EventGeneratorTest.php +++ b/tests/src/Kernel/EventGeneratorTest.php @@ -64,7 +64,7 @@ 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('language_manager')); } /** 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/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'); } } From b16bc2d9899de1ed782482603376cfd313775fb1 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Fri, 31 May 2019 10:18:40 -0300 Subject: [PATCH 35/70] Update composer.json --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 68cc2c71..74cc2a8b 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require": { "drupal/context": "^4.0", "drupal/search_api": "~1.8", - "islandora/jsonld": "0.2.0", + "islandora/jsonld": "dev-release", "stomp-php/stomp-php": "4.*", "drupal/jwt": "1.0.0-alpha6", "drupal/filehash": "^1.1", @@ -29,7 +29,7 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "islandora/crayfish-commons": "^0.0" + "islandora/crayfish-commons": "dev-release" }, "require-dev": { "phpunit/phpunit": "^6", From 2aa45758aaf4f1718e9ad616e9385ea811994f47 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Fri, 31 May 2019 15:33:45 +0000 Subject: [PATCH 36/70] Tweaking config --- .../install/core.entity_view_display.media.file.source.yml | 3 +++ .../core.entity_view_display.media.image.source.yml | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) 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..213f82e4 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 @@ -3,6 +3,7 @@ status: true dependencies: config: - core.entity_view_mode.media.source + - field.field.media.file.field_access_terms - field.field.media.file.field_file_size - field.field.media.file.field_media_file - field.field.media.file.field_media_of @@ -29,7 +30,9 @@ content: region: content hidden: created: true + field_access_terms: 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..5ecacfc9 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 @@ -3,6 +3,7 @@ status: true dependencies: config: - core.entity_view_mode.media.source + - field.field.media.image.field_access_terms - field.field.media.image.field_file_size - field.field.media.image.field_height - field.field.media.image.field_media_image @@ -15,7 +16,7 @@ dependencies: module: - islandora_core_feature module: - - image + - islandora id: media.image.source targetEntityType: media bundle: image @@ -27,12 +28,14 @@ content: image_style: '' image_link: content third_party_settings: { } - type: image + type: islandora_image weight: 0 region: content hidden: created: true + field_access_terms: true field_file_size: true + field_gemini_uri: true field_height: true field_media_of: true field_media_use: true From d291dbca8d04565cb2a3b64087d396f8caa2b823 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Fri, 31 May 2019 15:49:11 +0000 Subject: [PATCH 37/70] Pinning to 1.0.0 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 74cc2a8b..3b38bb61 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require": { "drupal/context": "^4.0", "drupal/search_api": "~1.8", - "islandora/jsonld": "dev-release", + "islandora/jsonld": "1.0.0", "stomp-php/stomp-php": "4.*", "drupal/jwt": "1.0.0-alpha6", "drupal/filehash": "^1.1", @@ -29,7 +29,7 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "islandora/crayfish-commons": "dev-release" + "islandora/crayfish-commons": "1.0.0" }, "require-dev": { "phpunit/phpunit": "^6", From 23b431a3e8db3e0645c8403162f08e679c0dbea0 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Fri, 31 May 2019 16:17:17 -0300 Subject: [PATCH 38/70] Getting rid of islandora_defaults dependency --- .../install/core.entity_view_display.media.file.source.yml | 2 -- .../install/core.entity_view_display.media.image.source.yml | 2 -- 2 files changed, 4 deletions(-) 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 213f82e4..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 @@ -3,7 +3,6 @@ status: true dependencies: config: - core.entity_view_mode.media.source - - field.field.media.file.field_access_terms - field.field.media.file.field_file_size - field.field.media.file.field_media_file - field.field.media.file.field_media_of @@ -30,7 +29,6 @@ content: region: content hidden: created: true - field_access_terms: true field_file_size: true field_gemini_uri: true field_media_of: 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 5ecacfc9..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 @@ -3,7 +3,6 @@ status: true dependencies: config: - core.entity_view_mode.media.source - - field.field.media.image.field_access_terms - field.field.media.image.field_file_size - field.field.media.image.field_height - field.field.media.image.field_media_image @@ -33,7 +32,6 @@ content: region: content hidden: created: true - field_access_terms: true field_file_size: true field_gemini_uri: true field_height: true From 629cab8c7cdf403396ab8a39d4318994230a4d57 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Fri, 31 May 2019 16:18:20 -0300 Subject: [PATCH 39/70] Removing all occurences of access field --- .../islandora_core_feature/islandora_core_feature.features.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/islandora_core_feature/islandora_core_feature.features.yml b/modules/islandora_core_feature/islandora_core_feature.features.yml index 425bfa80..c3ff6620 100755 --- 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 From 28c74495c8a2d5a2b63d02b90a973027f0da8caa Mon Sep 17 00:00:00 2001 From: dannylamb Date: Mon, 3 Jun 2019 17:58:15 +0000 Subject: [PATCH 40/70] Adding describes url to messages and enforcing language neutrality in a few more places. --- islandora.services.yml | 6 ++-- src/EventGenerator/EventGenerator.php | 32 +++++++++++++++++-- src/EventSubscriber/LinkHeaderSubscriber.php | 14 +++++++- .../MediaLinkHeaderSubscriber.php | 3 +- .../NodeLinkHeaderSubscriber.php | 29 +++++++++++------ tests/src/Kernel/EventGeneratorTest.php | 27 ++++++++++------ 6 files changed, 85 insertions(+), 26 deletions(-) diff --git a/islandora.services.yml b/islandora.services.yml index fb7026bf..b88d186b 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -3,7 +3,7 @@ services: islandora.eventgenerator: class: Drupal\islandora\EventGenerator\EventGenerator - arguments: ['@language_manager'] + arguments: ['@language_manager', '@islandora.media_source_service'] islandora.stomp: class: Stomp\StatefulStomp factory: ['Drupal\islandora\StompFactory', create] @@ -16,12 +16,12 @@ 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', '@language_manager'] tags: - { name: event_subscriber } islandora.node_link_header_subscriber: class: Drupal\islandora\EventSubscriber\NodeLinkHeaderSubscriber - arguments: ['@entity_type.manager', '@entity_field.manager', '@access_manager', '@current_user', '@current_route_match', '@request_stack', '@islandora.utils'] + arguments: ['@entity_type.manager', '@entity_field.manager', '@access_manager', '@current_user', '@current_route_match', '@request_stack', '@language_manager', '@islandora.utils'] tags: - { name: event_subscriber } islandora.admin_view_route_subscriber: diff --git a/src/EventGenerator/EventGenerator.php b/src/EventGenerator/EventGenerator.php index ac258163..a9e4ac66 100644 --- a/src/EventGenerator/EventGenerator.php +++ b/src/EventGenerator/EventGenerator.php @@ -5,6 +5,7 @@ namespace Drupal\islandora\EventGenerator; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Url; +use Drupal\islandora\MediaSource\MediaSourceService; use Drupal\user\UserInterface; /** @@ -21,14 +22,24 @@ class EventGenerator implements EventGeneratorInterface { */ protected $languageManager; + /** + * Media source service. + * + * @var \Drupal\islandora\MediaSource\MediaSourceService + */ + protected $mediaSource; + /** * Constructor. * * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * Language manager. + * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source + * Media source service. */ - public function __construct(LanguageManagerInterface $language_manager) { + public function __construct(LanguageManagerInterface $language_manager, MediaSourceService $media_source) { $this->languageManager = $language_manager; + $this->mediaSource = $media_source; } /** @@ -39,12 +50,13 @@ class EventGenerator implements EventGeneratorInterface { $user_url = $user->toUrl()->setAbsolute()->toString(); $entity_type = $entity->getEntityTypeId(); + $undefined = $this->languageManager->getLanguage('und'); + if ($entity_type == 'file') { - $entity_url = $entity->url(); + $entity_url = $entity->url('canonical', ['absolute' => TRUE, 'language' => $undefined]); $mimetype = $entity->getMimeType(); } else { - $undefined = $this->languageManager->getLanguage('und'); $entity_url = Url::fromRoute( "rest.entity.$entity_type.GET", [$entity_type => $entity->id()], @@ -111,6 +123,20 @@ class EventGenerator implements EventGeneratorInterface { ]; } + // 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" => $file->url('canonical', ['absolute' => TRUE, 'language' => $undefined]), + "mediaType" => $file->getMimeType(), + "rel" => "describes", + ]; + } + } + unset($data["event"]); unset($data["queue"]); diff --git a/src/EventSubscriber/LinkHeaderSubscriber.php b/src/EventSubscriber/LinkHeaderSubscriber.php index 3ff64d4b..6bb184fd 100644 --- a/src/EventSubscriber/LinkHeaderSubscriber.php +++ b/src/EventSubscriber/LinkHeaderSubscriber.php @@ -6,6 +6,7 @@ use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Entity\EntityFieldManager; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; @@ -64,6 +65,13 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { */ protected $requestStack; + /** + * Language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + /** * Constructor. * @@ -79,6 +87,8 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { * The route match object. * @param Symfony\Component\HttpFoundation\RequestStack $request_stack * Request stack (for current request). + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * Language manager. */ public function __construct( EntityTypeManager $entity_type_manager, @@ -86,7 +96,8 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { AccessManagerInterface $access_manager, AccountInterface $account, RouteMatchInterface $route_match, - RequestStack $request_stack + RequestStack $request_stack, + LanguageManagerInterface $language_manager ) { $this->entityTypeManager = $entity_type_manager; $this->entityFieldManager = $entity_field_manager; @@ -96,6 +107,7 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { $this->accessManager = $access_manager; $this->account = $account; $this->requestStack = $request_stack; + $this->languageManager = $language_manager; } /** diff --git a/src/EventSubscriber/MediaLinkHeaderSubscriber.php b/src/EventSubscriber/MediaLinkHeaderSubscriber.php index 16e286e2..6e344f0d 100644 --- a/src/EventSubscriber/MediaLinkHeaderSubscriber.php +++ b/src/EventSubscriber/MediaLinkHeaderSubscriber.php @@ -77,9 +77,10 @@ class MediaLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSub } // Collect file links for the media. + $undefined = $this->languageManager->getLanguage('und'); foreach ($media->get($source_field)->referencedEntities() as $referencedEntity) { if ($referencedEntity->access('view')) { - $file_url = $referencedEntity->url('canonical', ['absolute' => TRUE]); + $file_url = $referencedEntity->url('canonical', ['absolute' => TRUE, 'language' => $undefined]); $links[] = "<$file_url>; rel=\"describes\"; type=\"{$referencedEntity->getMimeType()}\""; } } diff --git a/src/EventSubscriber/NodeLinkHeaderSubscriber.php b/src/EventSubscriber/NodeLinkHeaderSubscriber.php index e49b4cc1..4d898f8f 100644 --- a/src/EventSubscriber/NodeLinkHeaderSubscriber.php +++ b/src/EventSubscriber/NodeLinkHeaderSubscriber.php @@ -5,8 +5,10 @@ namespace Drupal\islandora\EventSubscriber; use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Entity\EntityFieldManager; use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Url; use Drupal\islandora\IslandoraUtils; use Drupal\node\NodeInterface; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; @@ -42,6 +44,8 @@ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubs * The route match object. * @param Symfony\Component\HttpFoundation\RequestStack $request_stack * Request stack (for current request). + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * Language manager. * @param \Drupal\islandora\IslandoraUtils $utils * Derivative utils. */ @@ -52,16 +56,18 @@ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubs AccountInterface $account, RouteMatchInterface $route_match, RequestStack $request_stack, + LanguageManagerInterface $language_manager, 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; + parent::__construct( + $entity_type_manager, + $entity_field_manager, + $access_manager, + $account, + $route_match, + $request_stack, + $language_manager + ); $this->utils = $utils; } @@ -99,8 +105,13 @@ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubs */ protected function generateRelatedMediaLinks(NodeInterface $node) { $links = []; + $undefined = $this->languageManager->getLanguage('und'); foreach ($this->utils->getMedia($node) as $media) { - $url = $media->url('canonical', ['absolute' => TRUE]); + $url = Url::fromRoute( + "rest.entity.media.GET", + ['media' => $media->id()], + ['absolute' => TRUE, 'language' => $undefined] + )->toString(); foreach ($media->referencedEntities() as $term) { if ($term->getEntityTypeId() == 'taxonomy_term' && $term->hasField('field_external_uri')) { $field = $term->get('field_external_uri'); diff --git a/tests/src/Kernel/EventGeneratorTest.php b/tests/src/Kernel/EventGeneratorTest.php index c5703f3f..21491be0 100644 --- a/tests/src/Kernel/EventGeneratorTest.php +++ b/tests/src/Kernel/EventGeneratorTest.php @@ -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->container->get('language_manager')); + $this->eventGenerator = new EventGenerator( + $this->container->get('language_manager'), + $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" + ); } } From 533dead54efa8a43ebecf904116945b80e9bea1e Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 6 Jun 2019 15:04:35 -0300 Subject: [PATCH 41/70] Prepare for next development iteration --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 3b38bb61..b59bf0f5 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require": { "drupal/context": "^4.0", "drupal/search_api": "~1.8", - "islandora/jsonld": "1.0.0", + "islandora/jsonld": "^1.0", "stomp-php/stomp-php": "4.*", "drupal/jwt": "1.0.0-alpha6", "drupal/filehash": "^1.1", @@ -29,7 +29,7 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "islandora/crayfish-commons": "1.0.0" + "islandora/crayfish-commons": "^1.0" }, "require-dev": { "phpunit/phpunit": "^6", From e3446c046bcf08afd65dbba75f3d6449c98087a4 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Mon, 10 Jun 2019 15:55:26 -0300 Subject: [PATCH 42/70] Pointing to dev instead of ^1.0 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b59bf0f5..41d76a2f 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "require": { "drupal/context": "^4.0", "drupal/search_api": "~1.8", - "islandora/jsonld": "^1.0", + "islandora/jsonld": "dev-8.x-1.x", "stomp-php/stomp-php": "4.*", "drupal/jwt": "1.0.0-alpha6", "drupal/filehash": "^1.1", @@ -29,7 +29,7 @@ "drupal/migrate_source_csv" : "^2.1", "drupal/token" : "^1.3", "drupal/flysystem" : "^1.0", - "islandora/crayfish-commons": "^1.0" + "islandora/crayfish-commons": "dev-dev" }, "require-dev": { "phpunit/phpunit": "^6", From 2c596577612cfd2b2c9a2f933150a0799dac6ef3 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Tue, 11 Jun 2019 12:30:45 -0500 Subject: [PATCH 43/70] use common testing scripts (#142) --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 80dbcb06..5f0f82b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,10 +26,8 @@ install: - cd web; drush --uri=127.0.0.1:8282 en -y islandora 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" notifications: irc: From 53d977e434bed3197f19ab7815448e5551428c20 Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Tue, 11 Jun 2019 12:01:27 -0700 Subject: [PATCH 44/70] Breadcrumbs sub-module (#132) * initial commit * break looped chains * add depthLimit and includeSelf configuration * clean up configuration * breadcrumb tests * enable tests in travis * fix travis tests --- .travis.yml | 1 + .../config/install/islandora.breadcrumbs.yml | 3 + .../schema/islandora_breadcrumbs.schema.yml | 12 +++ .../islandora_breadcrumbs.info.yml | 7 ++ .../islandora_breadcrumbs.services.yml | 6 ++ .../src/IslandoraBreadcrumbBuilder.php | 96 +++++++++++++++++++ .../tests/src/Functional/BreadcrumbsTest.php | 89 +++++++++++++++++ 7 files changed, 214 insertions(+) create mode 100644 modules/islandora_breadcrumbs/config/install/islandora.breadcrumbs.yml create mode 100644 modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml create mode 100644 modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml create mode 100644 modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml create mode 100644 modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php create mode 100644 modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php diff --git a/.travis.yml b/.travis.yml index 5f0f82b6..82b526f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ install: script: - $SCRIPT_DIR/travis_scripts.sh - $SCRIPT_DIR/run-tests.sh "islandora" + - $SCRIPT_DIR/run-tests.sh "islandora_breadcrumbs" notifications: irc: 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..af9efa06 --- /dev/null +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml @@ -0,0 +1,6 @@ +services: + islandora_breadcrumbs.breadcrumb: + class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder + arguments: ['@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..763c523a --- /dev/null +++ b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php @@ -0,0 +1,96 @@ +config = $config_factory->get('islandora.breadcrumbs'); + } + + /** + * {@inheritdoc} + */ + public function applies(RouteMatchInterface $attributes) { + $parameters = $attributes->getParameters()->all(); + if (!empty($parameters['node'])) { + return ($parameters['node']->hasField($this->config->get('referenceField')) && + !$parameters['node']->get($this->config->get('referenceField'))->isEmpty()); + } + } + + /** + * {@inheritdoc} + */ + public function build(RouteMatchInterface $route_match) { + + $node = $route_match->getParameter('node'); + $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..4ba101fe --- /dev/null +++ b/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php @@ -0,0 +1,89 @@ +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); + } + +} From 9af957f4e3f2e7658e0ac11cbbcec54c6f885d01 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Tue, 25 Jun 2019 16:30:46 -0400 Subject: [PATCH 45/70] Refactoring URL generation into util functions (#144) * Refactoring URL generation into util functions * Coding standards * Ignoring schema errors on media.settings in functional tests * Missed tests in submodules --- islandora.services.yml | 8 +- .../GenerateAudioDerivativeTest.php | 3 + .../tests/src/Functional/BreadcrumbsTest.php | 20 +++++ .../GenerateImageDerivativeTest.php | 3 + .../GenerateVideoDerivativeTest.php | 3 + .../NormalizerAlterReaction.php | 21 ++++-- src/Controller/MediaSourceController.php | 19 ++++- src/EventGenerator/EventGenerator.php | 36 ++++----- src/EventSubscriber/LinkHeaderSubscriber.php | 30 ++++---- .../MediaLinkHeaderSubscriber.php | 3 +- .../NodeLinkHeaderSubscriber.php | 65 +--------------- src/Flysystem/Adapter/FedoraAdapter.php | 12 ++- src/Flysystem/Fedora.php | 10 +++ src/IslandoraUtils.php | 74 ++++++++++++++++++- .../Action/AbstractGenerateDerivative.php | 2 +- .../MappingUriPredicateReaction.php | 13 +++- src/PresetReaction/PresetReaction.php | 5 ++ .../Functional/GenerateDerivativeTestBase.php | 3 + .../IslandoraFunctionalTestBase.php | 26 +++++++ tests/src/Kernel/EventGeneratorTest.php | 2 +- tests/src/Kernel/GeminiClientFactoryTest.php | 5 ++ tests/src/Kernel/GeminiLookupTest.php | 35 +++++++++ 22 files changed, 276 insertions(+), 122 deletions(-) diff --git a/islandora.services.yml b/islandora.services.yml index b88d186b..b08e0699 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -3,7 +3,7 @@ services: islandora.eventgenerator: class: Drupal\islandora\EventGenerator\EventGenerator - arguments: ['@language_manager', '@islandora.media_source_service'] + arguments: ['@islandora.utils', '@islandora.media_source_service'] islandora.stomp: class: Stomp\StatefulStomp factory: ['Drupal\islandora\StompFactory', create] @@ -16,12 +16,12 @@ 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', '@language_manager'] + 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: class: Drupal\islandora\EventSubscriber\NodeLinkHeaderSubscriber - arguments: ['@entity_type.manager', '@entity_field.manager', '@access_manager', '@current_user', '@current_route_match', '@request_stack', '@language_manager', '@islandora.utils'] + arguments: ['@entity_type.manager', '@entity_field.manager', '@access_manager', '@current_user', '@current_route_match', '@request_stack', '@islandora.utils'] tags: - { name: event_subscriber } islandora.admin_view_route_subscriber: @@ -51,7 +51,7 @@ 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] diff --git a/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php b/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php index 9c7f4e87..bcd78f70 100644 --- a/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php +++ b/modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php @@ -11,6 +11,9 @@ use Drupal\Tests\islandora\Functional\GenerateDerivativeTestBase; */ class GenerateAudioDerivativeTest extends GenerateDerivativeTestBase { + /** + * {@inheritdoc} + */ protected static $modules = ['context_ui', 'islandora_audio']; /** diff --git a/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php b/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php index 4ba101fe..c95d362c 100644 --- a/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php +++ b/modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php @@ -24,12 +24,32 @@ class BreadcrumbsTest extends IslandoraFunctionalTestBase { ]; + /** + * A node. + * + * @var \Drupal\node\NodeInterface + */ protected $nodeA; + /** + * Another node. + * + * @var \Drupal\node\NodeInterface + */ protected $nodeB; + /** + * Yet another node. + * + * @var \Drupal\node\NodeInterface + */ protected $nodeC; + /** + * Another one. + * + * @var \Drupal\node\NodeInterface + */ protected $nodeD; /** diff --git a/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php b/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php index 69e18ba7..c917ba6c 100644 --- a/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php +++ b/modules/islandora_image/tests/src/Functional/GenerateImageDerivativeTest.php @@ -11,6 +11,9 @@ use Drupal\Tests\islandora\Functional\GenerateDerivativeTestBase; */ class GenerateImageDerivativeTest extends GenerateDerivativeTestBase { + /** + * {@inheritdoc} + */ protected static $modules = ['context_ui', 'islandora_image']; /** diff --git a/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php b/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php index 64bd6d4a..02beca06 100644 --- a/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php +++ b/modules/islandora_video/tests/src/Functional/GenerateVideoDerivativeTest.php @@ -11,6 +11,9 @@ use Drupal\Tests\islandora\Functional\GenerateDerivativeTestBase; */ class GenerateVideoDerivativeTest extends GenerateDerivativeTestBase { + /** + * {@inheritdoc} + */ protected static $modules = ['context_ui', 'islandora_video']; /** diff --git a/src/ContextReaction/NormalizerAlterReaction.php b/src/ContextReaction/NormalizerAlterReaction.php index 503c7248..1dcd5c68 100644 --- a/src/ContextReaction/NormalizerAlterReaction.php +++ b/src/ContextReaction/NormalizerAlterReaction.php @@ -7,6 +7,7 @@ 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; /** @@ -25,16 +26,25 @@ abstract class NormalizerAlterReaction extends ContextReactionPluginBase impleme */ protected $jsonldConfig; + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + /** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition, - ConfigFactoryInterface $config_factory) { + ConfigFactoryInterface $config_factory, + IslandoraUtils $utils) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->jsonldConfig = $config_factory->get(JsonLdSettingsForm::CONFIG_NAME); + $this->utils = $utils; } /** @@ -45,7 +55,8 @@ abstract class NormalizerAlterReaction extends ContextReactionPluginBase impleme $configuration, $plugin_id, $plugin_definition, - $container->get('config.factory') + $container->get('config.factory'), + $container->get('islandora.utils') ); } @@ -71,11 +82,11 @@ abstract class NormalizerAlterReaction extends ContextReactionPluginBase impleme * The url. */ protected function getSubjectUrl(EntityInterface $entity) { - $url = $entity->toUrl('canonical', ['absolute' => TRUE]); + $format = ''; if (!$this->jsonldConfig->get(JsonLdSettingsForm::REMOVE_JSONLD_FORMAT)) { - $url->setRouteParameter('_format', 'jsonld'); + $format = 'jsonld'; } - return $url->toString(); + return $this->utils->getRestUrl($entity, $format); } } 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 a9e4ac66..bf95e97d 100644 --- a/src/EventGenerator/EventGenerator.php +++ b/src/EventGenerator/EventGenerator.php @@ -3,8 +3,7 @@ namespace Drupal\islandora\EventGenerator; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\Url; +use Drupal\islandora\IslandoraUtils; use Drupal\islandora\MediaSource\MediaSourceService; use Drupal\user\UserInterface; @@ -16,11 +15,11 @@ use Drupal\user\UserInterface; class EventGenerator implements EventGeneratorInterface { /** - * Language manager. + * Islandora utils. * - * @var \Drupal\Core\Language\LanguageManagerInterface + * @var \Drupal\islandora\IslandoraUtils */ - protected $languageManager; + protected $utils; /** * Media source service. @@ -32,13 +31,13 @@ class EventGenerator implements EventGeneratorInterface { /** * Constructor. * - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * Language manager. + * @param \Drupal\islandora\IslandoraUtils $utils + * Islandora utils. * @param \Drupal\islandora\MediaSource\MediaSourceService $media_source * Media source service. */ - public function __construct(LanguageManagerInterface $language_manager, MediaSourceService $media_source) { - $this->languageManager = $language_manager; + public function __construct(IslandoraUtils $utils, MediaSourceService $media_source) { + $this->utils = $utils; $this->mediaSource = $media_source; } @@ -47,21 +46,16 @@ class EventGenerator implements EventGeneratorInterface { */ public function generateEvent(EntityInterface $entity, UserInterface $user, array $data) { - $user_url = $user->toUrl()->setAbsolute()->toString(); - $entity_type = $entity->getEntityTypeId(); + $user_url = $this->utils->getEntityUrl($user); - $undefined = $this->languageManager->getLanguage('und'); + $entity_type = $entity->getEntityTypeId(); if ($entity_type == 'file') { - $entity_url = $entity->url('canonical', ['absolute' => TRUE, 'language' => $undefined]); + $entity_url = $this->utils->getDownloadUrl($entity); $mimetype = $entity->getMimeType(); } else { - $entity_url = Url::fromRoute( - "rest.entity.$entity_type.GET", - [$entity_type => $entity->id()], - ['absolute' => TRUE, 'language' => $undefined] - )->toString(); + $entity_url = $this->utils->getEntityUrl($entity); $mimetype = 'text/html'; } @@ -110,14 +104,14 @@ 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", ]; @@ -130,7 +124,7 @@ class EventGenerator implements EventGeneratorInterface { $event['object']['url'][] = [ "name" => "Describes", "type" => "Link", - "href" => $file->url('canonical', ['absolute' => TRUE, 'language' => $undefined]), + "href" => $this->utils->getDownloadUrl($file), "mediaType" => $file->getMimeType(), "rel" => "describes", ]; diff --git a/src/EventSubscriber/LinkHeaderSubscriber.php b/src/EventSubscriber/LinkHeaderSubscriber.php index 6bb184fd..4edf7da4 100644 --- a/src/EventSubscriber/LinkHeaderSubscriber.php +++ b/src/EventSubscriber/LinkHeaderSubscriber.php @@ -6,10 +6,9 @@ use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Entity\EntityFieldManager; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManager; -use Drupal\Core\Language\LanguageManagerInterface; 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; @@ -66,11 +65,11 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { protected $requestStack; /** - * Language manager. + * Islandora utils. * - * @var \Drupal\Core\Language\LanguageManagerInterface + * @var \Drupal\islandora\IslandoraUtils */ - protected $languageManager; + protected $utils; /** * Constructor. @@ -87,8 +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\Core\Language\LanguageManagerInterface $language_manager - * Language manager. + * @param \Drupal\islandora\IslandoraUtils $utils + * Islandora utils. */ public function __construct( EntityTypeManager $entity_type_manager, @@ -97,7 +96,7 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { AccountInterface $account, RouteMatchInterface $route_match, RequestStack $request_stack, - LanguageManagerInterface $language_manager + IslandoraUtils $utils ) { $this->entityTypeManager = $entity_type_manager; $this->entityFieldManager = $entity_field_manager; @@ -107,7 +106,7 @@ abstract class LinkHeaderSubscriber implements EventSubscriberInterface { $this->accessManager = $access_manager; $this->account = $account; $this->requestStack = $request_stack; - $this->languageManager = $language_manager; + $this->utils = $utils; } /** @@ -206,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. @@ -213,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'])) { @@ -228,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\""; @@ -287,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 6e344f0d..644db872 100644 --- a/src/EventSubscriber/MediaLinkHeaderSubscriber.php +++ b/src/EventSubscriber/MediaLinkHeaderSubscriber.php @@ -77,10 +77,9 @@ class MediaLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSub } // Collect file links for the media. - $undefined = $this->languageManager->getLanguage('und'); foreach ($media->get($source_field)->referencedEntities() as $referencedEntity) { if ($referencedEntity->access('view')) { - $file_url = $referencedEntity->url('canonical', ['absolute' => TRUE, 'language' => $undefined]); + $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 4d898f8f..443ce86f 100644 --- a/src/EventSubscriber/NodeLinkHeaderSubscriber.php +++ b/src/EventSubscriber/NodeLinkHeaderSubscriber.php @@ -2,17 +2,8 @@ namespace Drupal\islandora\EventSubscriber; -use Drupal\Core\Access\AccessManagerInterface; -use Drupal\Core\Entity\EntityFieldManager; -use Drupal\Core\Entity\EntityTypeManager; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Url; -use Drupal\islandora\IslandoraUtils; use Drupal\node\NodeInterface; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -22,55 +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\Core\Language\LanguageManagerInterface $language_manager - * Language manager. - * @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, - LanguageManagerInterface $language_manager, - IslandoraUtils $utils - ) { - parent::__construct( - $entity_type_manager, - $entity_field_manager, - $access_manager, - $account, - $route_match, - $request_stack, - $language_manager - ); - $this->utils = $utils; - } - /** * Adds node-specific link headers to appropriate responses. * @@ -105,13 +47,8 @@ class NodeLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSubs */ protected function generateRelatedMediaLinks(NodeInterface $node) { $links = []; - $undefined = $this->languageManager->getLanguage('und'); foreach ($this->utils->getMedia($node) as $media) { - $url = Url::fromRoute( - "rest.entity.media.GET", - ['media' => $media->id()], - ['absolute' => TRUE, 'language' => $undefined] - )->toString(); + $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 85f42b1a..b90fc529 100644 --- a/src/Flysystem/Fedora.php +++ b/src/Flysystem/Fedora.php @@ -27,8 +27,18 @@ 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; /** 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/Plugin/Action/AbstractGenerateDerivative.php b/src/Plugin/Action/AbstractGenerateDerivative.php index 4635a95c..233ce774 100644 --- a/src/Plugin/Action/AbstractGenerateDerivative.php +++ b/src/Plugin/Action/AbstractGenerateDerivative.php @@ -155,7 +155,7 @@ class AbstractGenerateDerivative extends EmitEvent { throw new \RuntimeException("Could not locate source file for media {$source_media->id()}", 500); } - $data['source_uri'] = $source_file->url('canonical', ['absolute' => TRUE]); + $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. diff --git a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php index c59e48c8..05f2f8c0 100644 --- a/src/Plugin/ContextReaction/MappingUriPredicateReaction.php +++ b/src/Plugin/ContextReaction/MappingUriPredicateReaction.php @@ -9,6 +9,7 @@ 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; /** @@ -23,6 +24,11 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction { const URI_PREDICATE = 'drupal_uri_predicate'; + /** + * Media source service. + * + * @var \Drupal\islandora\MediaSource\MediaSourceService + */ protected $mediaSource; /** @@ -32,13 +38,15 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction { $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory, + IslandoraUtils $utils, MediaSourceService $media_source) { parent::__construct( $configuration, $plugin_id, $plugin_definition, - $config_factory + $config_factory, + $utils ); $this->mediaSource = $media_source; } @@ -52,6 +60,7 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction { $plugin_id, $plugin_definition, $container->get('config.factory'), + $container->get('islandora.utils'), $container->get('islandora.media_source_service') ); } @@ -80,7 +89,7 @@ class MappingUriPredicateReaction extends NormalizerAlterReaction { // Swap media and file urls. if ($entity instanceof MediaInterface) { $file = $this->mediaSource->getSourceFile($entity); - $graph['@id'] = $file->url('canonical', ['absolute' => TRUE]); + $graph['@id'] = $this->utils->getDownloadUrl($file); } if (isset($graph[$drupal_predicate])) { if (!is_array($graph[$drupal_predicate])) { 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/src/Functional/GenerateDerivativeTestBase.php b/tests/src/Functional/GenerateDerivativeTestBase.php index b650ca3b..c74ff255 100644 --- a/tests/src/Functional/GenerateDerivativeTestBase.php +++ b/tests/src/Functional/GenerateDerivativeTestBase.php @@ -7,6 +7,9 @@ namespace Drupal\Tests\islandora\Functional; */ abstract class GenerateDerivativeTestBase extends IslandoraFunctionalTestBase { + /** + * {@inheritdoc} + */ protected static $modules = ['context_ui']; /** diff --git a/tests/src/Functional/IslandoraFunctionalTestBase.php b/tests/src/Functional/IslandoraFunctionalTestBase.php index e1d6e91f..9a50fb05 100644 --- a/tests/src/Functional/IslandoraFunctionalTestBase.php +++ b/tests/src/Functional/IslandoraFunctionalTestBase.php @@ -20,8 +20,14 @@ class IslandoraFunctionalTestBase extends BrowserTestBase { use TestFileCreationTrait; use MediaTypeCreationTrait; + /** + * {@inheritdoc} + */ protected static $modules = ['context_ui', 'field_ui', 'islandora']; + /** + * {@inheritdoc} + */ protected static $configSchemaCheckerExclusions = [ 'jwt.config', 'context.context.test', @@ -29,12 +35,28 @@ 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; /** @@ -170,6 +192,10 @@ 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(); diff --git a/tests/src/Kernel/EventGeneratorTest.php b/tests/src/Kernel/EventGeneratorTest.php index 21491be0..c423cda3 100644 --- a/tests/src/Kernel/EventGeneratorTest.php +++ b/tests/src/Kernel/EventGeneratorTest.php @@ -65,7 +65,7 @@ class EventGeneratorTest extends IslandoraKernelTestBase { // Create the event generator so we can test it. $this->eventGenerator = new EventGenerator( - $this->container->get('language_manager'), + $this->container->get('islandora.utils'), $this->container->get('islandora.media_source_service') ); } diff --git a/tests/src/Kernel/GeminiClientFactoryTest.php b/tests/src/Kernel/GeminiClientFactoryTest.php index 3259c221..1bbd8e75 100644 --- a/tests/src/Kernel/GeminiClientFactoryTest.php +++ b/tests/src/Kernel/GeminiClientFactoryTest.php @@ -18,6 +18,11 @@ use Psr\Log\LoggerInterface; */ class GeminiClientFactoryTest extends IslandoraKernelTestBase { + /** + * Logger. + * + * @var \Psr\Log\LoggerInterface + */ private $logger; /** diff --git a/tests/src/Kernel/GeminiLookupTest.php b/tests/src/Kernel/GeminiLookupTest.php index 600fea73..15edc2e8 100644 --- a/tests/src/Kernel/GeminiLookupTest.php +++ b/tests/src/Kernel/GeminiLookupTest.php @@ -23,18 +23,53 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; */ class GeminiLookupTest extends IslandoraKernelTestBase { + /** + * JWT Auth. + * + * @var \Drupal\jwt\Authentication\Provider\JwtAuth + */ private $jwtAuth; + /** + * Logger. + * + * @var \Psr\Log\LoggerInterface + */ private $logger; + /** + * Guzzle. + * + * @var \GuzzleHttp\Client + */ private $guzzle; + /** + * Gemini client. + * + * @var \Islandora\Crayfish\Commons\Client\GeminiClient + */ private $geminiClient; + /** + * Media source service. + * + * @var \Drupal\islandora\MediaSource\MediaSourceService + */ private $mediaSource; + /** + * An entity. + * + * @var \Drupal\Core\Entity\EntityInterface + */ private $entity; + /** + * A media. + * + * @var \Drupal\media\MediaInterface + */ private $media; /** From 7f3ce9f092854b751bf58b5f3623cb21dc1b61b9 Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Wed, 26 Jun 2019 11:33:11 -0700 Subject: [PATCH 46/70] change cardinality (#143) --- .../config/install/field.storage.media.field_media_use.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f17b5d44e6c39f5969fcda7e1e6adb0b1b77a833 Mon Sep 17 00:00:00 2001 From: Mark Jordan Date: Wed, 26 Jun 2019 12:04:36 -0700 Subject: [PATCH 47/70] Adds a Context Condition to detect the MIME type of the media (#145) --- config/schema/islandora.schema.yml | 7 + islandora.module | 1 + src/Plugin/Condition/MediaHasMimetype.php | 174 ++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 src/Plugin/Condition/MediaHasMimetype.php diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index 1df5b7e8..9c2d33fe 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -113,6 +113,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: diff --git a/islandora.module b/islandora.module index 8b0d52e1..5d55da16 100644 --- a/islandora.module +++ b/islandora.module @@ -347,6 +347,7 @@ function islandora_form_block_form_alter(&$form, FormStateInterface $form_state, unset($form['visibility']['file_uses_filesystem']); unset($form['visibility']['node_has_term']); unset($form['visibility']['media_uses_filesystem']); + unset($form['visibility']['media_has_mimetype']); } /** 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() + ); + } + +} From 9bb4635cfa656ecb75250e17854135fd5804549d Mon Sep 17 00:00:00 2001 From: Mark Jordan Date: Wed, 26 Jun 2019 12:12:46 -0700 Subject: [PATCH 48/70] Adds a Context Condition that tests a node's parent (#146) --- config/schema/islandora.schema.yml | 10 ++ islandora.module | 1 + src/Plugin/Condition/NodeHasParent.php | 168 +++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 src/Plugin/Condition/NodeHasParent.php diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index 9c2d33fe..ade716c3 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -83,6 +83,16 @@ condition.plugin.node_has_term: 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: diff --git a/islandora.module b/islandora.module index 5d55da16..3a20e6b2 100644 --- a/islandora.module +++ b/islandora.module @@ -346,6 +346,7 @@ function islandora_form_block_form_alter(&$form, FormStateInterface $form_state, 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']); } 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']]); + } + } + +} From d65e25f59cde468e853d46f7023d7ccf43fa9d7f Mon Sep 17 00:00:00 2001 From: Mark Jordan Date: Wed, 26 Jun 2019 12:22:07 -0700 Subject: [PATCH 49/70] Create a Context Condition that detects the namespace in the PID field (#147) --- config/schema/islandora.schema.yml | 10 ++ islandora.module | 1 + src/Plugin/Condition/NodeHadNamespace.php | 187 ++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 src/Plugin/Condition/NodeHadNamespace.php diff --git a/config/schema/islandora.schema.yml b/config/schema/islandora.schema.yml index ade716c3..7b89a25c 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -138,6 +138,16 @@ condition.plugin.content_entity_type: 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' diff --git a/islandora.module b/islandora.module index 3a20e6b2..183adabc 100644 --- a/islandora.module +++ b/islandora.module @@ -343,6 +343,7 @@ 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']); diff --git a/src/Plugin/Condition/NodeHadNamespace.php b/src/Plugin/Condition/NodeHadNamespace.php new file mode 100644 index 00000000..eb0f43d4 --- /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) { + if ($entity->hasField('field_pid')) { + $pid_field = $this->configuration['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() + ); + } + +} From 2e0d66f6b1b182c24d5389add47dfa848a7711b4 Mon Sep 17 00:00:00 2001 From: Melissa Anez Date: Fri, 28 Jun 2019 10:52:44 -0300 Subject: [PATCH 50/70] Update CONTRIBUTING.md (#148) --- CONTRIBUTING.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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). From a956f5a4a9be9ed428073d509492f0f1685d7993 Mon Sep 17 00:00:00 2001 From: Mark Jordan Date: Tue, 2 Jul 2019 09:45:37 -0700 Subject: [PATCH 51/70] Work on #1199. (#149) --- src/Plugin/Condition/NodeHadNamespace.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugin/Condition/NodeHadNamespace.php b/src/Plugin/Condition/NodeHadNamespace.php index eb0f43d4..a033913a 100644 --- a/src/Plugin/Condition/NodeHadNamespace.php +++ b/src/Plugin/Condition/NodeHadNamespace.php @@ -145,8 +145,8 @@ class NodeHadNamespace extends ConditionPluginBase implements ContainerFactoryPl * TRUE if entity has the specified namespace, otherwise FALSE. */ protected function evaluateEntity(EntityInterface $entity) { - if ($entity->hasField('field_pid')) { - $pid_field = $this->configuration['pid_field']; + $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, ':') . ':'; From fef6b30eab3b0c365e82c902122f67f2da152ca7 Mon Sep 17 00:00:00 2001 From: Melissa Anez Date: Wed, 3 Jul 2019 17:48:19 -0300 Subject: [PATCH 52/70] Add in-form help text for Media Use field on the four standard media types (#150) * Update field.field.media.image.field_media_use.yml * Update field.field.media.audio.field_media_use.yml * Update field.field.media.file.field_media_use.yml * Update field.field.media.video.field_media_use.yml --- .../config/install/field.field.media.audio.field_media_use.yml | 2 +- .../config/install/field.field.media.file.field_media_use.yml | 2 +- .../config/install/field.field.media.image.field_media_use.yml | 2 +- .../config/install/field.field.media.video.field_media_use.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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: { } From 520d1de93c33657ce5e4004a0301e8ab5c089690 Mon Sep 17 00:00:00 2001 From: Melissa Anez Date: Mon, 15 Jul 2019 12:15:49 -0300 Subject: [PATCH 53/70] Update field.storage.node.field_display_hints.yml (#152) --- .../config/install/field.storage.node.field_display_hints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f6252999ae9ee179e759ead9709ac13358c608de Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Mon, 15 Jul 2019 18:27:51 -0700 Subject: [PATCH 54/70] Node Is Published Condition (#151) * Create NodeIsPublished.php * fix partial commit * phpcbf * fix constructors --- src/Plugin/Condition/NodeIsPublished.php | 94 ++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/Plugin/Condition/NodeIsPublished.php 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.'); + } + } + +} From 310481be343d4a7ae08921a1301efe9cf8a07c34 Mon Sep 17 00:00:00 2001 From: dannylamb Date: Thu, 18 Jul 2019 23:53:47 -0300 Subject: [PATCH 55/70] Remove duplicate media delete action (#153) * Removing duplicate delete media action * Torching test * Update hook * Coding standards --- config/install/system.action.delete_media.yml | 13 ------- config/schema/islandora.schema.yml | 4 --- islandora.install | 12 +++++++ src/Plugin/Action/DeleteMedia.php | 35 ------------------- tests/src/Functional/DeleteMediaTest.php | 25 ------------- 5 files changed, 12 insertions(+), 77 deletions(-) delete mode 100644 config/install/system.action.delete_media.yml delete mode 100644 src/Plugin/Action/DeleteMedia.php 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 7b89a25c..8fd719ce 100644 --- a/config/schema/islandora.schema.yml +++ b/config/schema/islandora.schema.yml @@ -68,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' 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/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/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. * From 5888d45b4153411941c07228cbba1de8bf270a32 Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Mon, 12 Aug 2019 06:48:20 -0600 Subject: [PATCH 56/70] Fix Breadcrumb error when viewing node tab: Issue 1238 (#155) * Fix applies when viewing a node's tab * remove logging message used during dev * clarify note about getRawParameters * update build to use getRawParameters * use dependency injection for node storage * TRAVIS! --- .travis.yml | 2 ++ .../islandora_breadcrumbs.services.yml | 2 +- .../src/IslandoraBreadcrumbBuilder.php | 27 ++++++++++++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 82b526f8..2743abda 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: php php: - 7.1 - 7.2 +services: + - mysql matrix: fast_finish: true diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml index af9efa06..58e3c959 100644 --- a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml @@ -1,6 +1,6 @@ services: islandora_breadcrumbs.breadcrumb: class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder - arguments: ['@config.factory'] + 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 index 763c523a..54fe9534 100644 --- a/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php +++ b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php @@ -3,6 +3,7 @@ namespace Drupal\islandora_breadcrumbs; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; @@ -20,13 +21,23 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { */ protected $config; + /** + * Storage to load nodes. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $nodeStorage; + /** * Constructs a breadcrumb builder. * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * Storage to load nodes. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The configuration factory. */ - public function __construct(ConfigFactoryInterface $config_factory) { + public function __construct(EntityTypeManagerInterface $entity_manager, ConfigFactoryInterface $config_factory) { + $this->nodeStorage = $entity_manager->getStorage('node'); $this->config = $config_factory->get('islandora.breadcrumbs'); } @@ -34,10 +45,13 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { * {@inheritdoc} */ public function applies(RouteMatchInterface $attributes) { - $parameters = $attributes->getParameters()->all(); - if (!empty($parameters['node'])) { - return ($parameters['node']->hasField($this->config->get('referenceField')) && - !$parameters['node']->get($this->config->get('referenceField'))->isEmpty()); + // 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()); } } @@ -46,7 +60,8 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { */ public function build(RouteMatchInterface $route_match) { - $node = $route_match->getParameter('node'); + $nid = $route_match->getRawParameters()->get('node'); + $node = $this->nodeStorage->load($nid); $breadcrumb = new Breadcrumb(); $chain = []; From f301c833cf0eeb395236587530fbbc274e124833 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Mon, 26 Aug 2019 12:49:40 -0500 Subject: [PATCH 57/70] Define a filesize attribute to be persisted (#156) --- islandora.module | 1 + .../config/install/rdf.mapping.media.audio.yml | 3 +++ .../config/install/rdf.mapping.media.file.yml | 3 +++ .../config/install/rdf.mapping.media.image.yml | 3 +++ .../config/install/rdf.mapping.media.video.yml | 3 +++ 5 files changed, 13 insertions(+) diff --git a/islandora.module b/islandora.module index 183adabc..38a83757 100644 --- a/islandora.module +++ b/islandora.module @@ -60,6 +60,7 @@ 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#', ]; } 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' From 6122f1df8327a2029614b09e24f0cd5de5ac488c Mon Sep 17 00:00:00 2001 From: dbernstein Date: Fri, 6 Sep 2019 11:10:22 -0700 Subject: [PATCH 58/70] Adds http://purl.org/co namespace to the islandora module. (#159) * Adds support for weight field in partial fulfillment of https://github.com/Islandora-CLAW/CLAW/issues/1261 * Removes unnecessary changes per PR feedback. --- islandora.module | 1 + 1 file changed, 1 insertion(+) diff --git a/islandora.module b/islandora.module index 38a83757..e39b4be4 100644 --- a/islandora.module +++ b/islandora.module @@ -61,6 +61,7 @@ function islandora_rdf_namespaces() { '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/', ]; } From c3fa84500ea716c61c667491dd6f1cebbeefff09 Mon Sep 17 00:00:00 2001 From: Jared Whiklo Date: Mon, 9 Sep 2019 09:18:43 -0500 Subject: [PATCH 59/70] Hide media and members tabs on non-Islandora objects. (#160) * Use custom access to test type of node * Also hide Members tab * Separate permission for node fields * Check for correct field * Add phpdoc and make both fields required. * Remove whitespace --- src/Controller/ManageMediaController.php | 28 +++++++++++++++++++ .../AdminViewsRouteSubscriber.php | 4 +++ 2 files changed, 32 insertions(+) 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/EventSubscriber/AdminViewsRouteSubscriber.php b/src/EventSubscriber/AdminViewsRouteSubscriber.php index 2d7eb95b..0ae1626c 100644 --- a/src/EventSubscriber/AdminViewsRouteSubscriber.php +++ b/src/EventSubscriber/AdminViewsRouteSubscriber.php @@ -16,9 +16,13 @@ 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'); } } From 7bd9ca17b53fc4eee5695e3934d9faadd4149c61 Mon Sep 17 00:00:00 2001 From: Minnie Rangel Date: Mon, 9 Sep 2019 21:08:57 -0500 Subject: [PATCH 60/70] adding tags for paged content (#161) 3 new tags to cover standard books and periodicals/newspapers and the sub Page model --- migrate/tags.csv | 3 +++ 1 file changed, 3 insertions(+) diff --git a/migrate/tags.csv b/migrate/tags.csv index c5054546..60705a2b 100644 --- a/migrate/tags.csv +++ b/migrate/tags.csv @@ -12,3 +12,6 @@ islandora_models,"Collection","A collection is an aggregation of items",http://p 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 From a00573f3edaf4ef7aeb00f2bba5ed71bd17c6dc9 Mon Sep 17 00:00:00 2001 From: Joe Corall Date: Wed, 11 Sep 2019 14:16:53 -0400 Subject: [PATCH 61/70] Initial implementation of a REST Export serializer for IIIF Manifests (#157) * Initial implementation of a REST Export serializer for IIIF Manifests * Coding standards * Pass full URL to IIIF * Basic implementation of manifest per https://iiif.io/api/presentation/2.1/#manifest * Remove unused variable * Remove dependency for OSD, add config variable for IIIF server * Update settings.yml * Fix key name * fix schema/settings issue * Broaden approach of discovering file/image fields --- .../install/islandora_iiif.settings.yml | 1 + .../config/schema/islandora_iiif.schema.yml | 7 + .../islandora_iiif/islandora_iiif.info.yml | 7 + .../islandora_iiif.links.menu.yml | 6 + modules/islandora_iiif/islandora_iiif.module | 24 ++ .../islandora_iiif/islandora_iiif.routing.yml | 9 + .../src/Form/IslandoraIIIFConfigForm.php | 92 ++++++ .../src/Plugin/views/style/IIIFManifest.php | 267 ++++++++++++++++++ 8 files changed, 413 insertions(+) create mode 100644 modules/islandora_iiif/config/install/islandora_iiif.settings.yml create mode 100644 modules/islandora_iiif/config/schema/islandora_iiif.schema.yml create mode 100644 modules/islandora_iiif/islandora_iiif.info.yml create mode 100644 modules/islandora_iiif/islandora_iiif.links.menu.yml create mode 100644 modules/islandora_iiif/islandora_iiif.module create mode 100644 modules/islandora_iiif/islandora_iiif.routing.yml create mode 100644 modules/islandora_iiif/src/Form/IslandoraIIIFConfigForm.php create mode 100644 modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php 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..167069df --- /dev/null +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -0,0 +1,267 @@ +serializer = $serializer; + $this->request = $request; + $this->iiifConfig = $iiif_config; + } + + /** + * {@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') + ); + } + + /** + * {@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, + '@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 = []; + $viewsField = $this->view->field[$this->options['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(); + $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(); + + $canvases[] = [ + // @see https://iiif.io/api/presentation/2.1/#canvas + '@id' => $canvas_id, + '@type' => 'sc:Canvas', + 'label' => $entity->label(), + // @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', + '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'), + '#type' => 'select', + '#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']; + } + +} From eb0014da4d5c35d448647f97e0d5a410e86a824e Mon Sep 17 00:00:00 2001 From: Joe Corall Date: Thu, 12 Sep 2019 15:56:36 -0400 Subject: [PATCH 62/70] Allow for multiple fields to be selected (#165) --- .../src/Plugin/views/style/IIIFManifest.php | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php index 167069df..e2b80520 100644 --- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -145,45 +145,47 @@ class IIIFManifest extends StylePluginBase { */ protected function getTileSourceFromRow(ResultRow $row, $iiif_address, $iiif_base_id) { $canvases = []; - $viewsField = $this->view->field[$this->options['iiif_tile_field']]; - $entity = $viewsField->getEntity($row); + 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']})) { + 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(); - $iiif_url = rtrim($iiif_address, '/') . '/' . urlencode($file_url); + /** @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(); + $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(); + // 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(); - $canvases[] = [ - // @see https://iiif.io/api/presentation/2.1/#canvas - '@id' => $canvas_id, - '@type' => 'sc:Canvas', - 'label' => $entity->label(), - // @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', - 'service' => [ - '@id' => $iiif_url, - '@context' => 'http://iiif.io/api/image/2/context.json', - 'profile' => 'http://iiif.io/api/image/2/profiles/level2.json', + $canvases[] = [ + // @see https://iiif.io/api/presentation/2.1/#canvas + '@id' => $canvas_id, + '@type' => 'sc:Canvas', + 'label' => $entity->label(), + // @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', + '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, ], - 'on' => $canvas_id, - ], - ], - ]; + ], + ]; + } } } @@ -242,8 +244,8 @@ class IIIFManifest extends StylePluginBase { } $form['iiif_tile_field'] = [ - '#title' => $this->t('Tile source field'), - '#type' => 'select', + '#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, From d16e49748a9f849288d4d294229d2f61fc26c34a Mon Sep 17 00:00:00 2001 From: Joe Corall Date: Wed, 18 Sep 2019 11:48:23 -0400 Subject: [PATCH 63/70] IIIF Presentation API Validator (#166) * Add a manifest label, and width/height of image per https://iiif.io/api/presentation/validator/service/ * try fetching image metadata from IIIF before using Drupal's settings * Move $mime_type into scope --- .../src/Plugin/views/style/IIIFManifest.php | 96 +++++++++++++++---- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php index e2b80520..0e33199b 100644 --- a/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php +++ b/modules/islandora_iiif/src/Plugin/views/style/IIIFManifest.php @@ -9,6 +9,10 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\HttpFoundation\Request; use Drupal\Core\Config\ImmutableConfig; +use Drupal\Core\File\FileSystem; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; /** * Provide serializer format for IIIF Manifest. @@ -62,15 +66,24 @@ class IIIFManifest extends StylePluginBase { */ protected $iiifConfig; + /** + * The Drupal Filesystem. + * + * @var \Drupal\Core\File\FileSystem + */ + protected $fileSystem; + /** * {@inheritdoc} */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, Request $request, ImmutableConfig $iiif_config) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, Request $request, ImmutableConfig $iiif_config, FileSystem $file_system, Client $http_client) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->serializer = $serializer; $this->request = $request; $this->iiifConfig = $iiif_config; + $this->fileSystem = $file_system; + $this->httpClient = $http_client; } /** @@ -83,7 +96,9 @@ class IIIFManifest extends StylePluginBase { $plugin_definition, $container->get('serializer'), $container->get('request_stack')->getCurrentRequest(), - $container->get('config.factory')->get('islandora_iiif.settings') + $container->get('config.factory')->get('islandora_iiif.settings'), + $container->get('file_system'), + $container->get('http_client') ); } @@ -105,12 +120,16 @@ class IIIFManifest extends StylePluginBase { $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', + [ + '@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. @@ -157,32 +176,73 @@ class IIIFManifest extends StylePluginBase { // 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' => $entity->label(), + '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', - 'service' => [ - '@id' => $iiif_url, - '@context' => 'http://iiif.io/api/image/2/context.json', - 'profile' => 'http://iiif.io/api/image/2/profiles/level2.json', + '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, ], - 'on' => $canvas_id, - ], ], ]; } From e0d70b6059593788d90cffe69150077b3cc5130c Mon Sep 17 00:00:00 2001 From: dannylamb Date: Wed, 18 Sep 2019 14:15:03 -0300 Subject: [PATCH 64/70] Allowing NodeHasTerm to check for multiple tags using AND logic (#167) --- src/Plugin/Condition/NodeHasTerm.php | 63 ++++++++++++----- tests/src/Functional/NodeHasTermTest.php | 90 ++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 tests/src/Functional/NodeHasTermTest.php 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/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"); + } + +} From 38a13c179f9a115c44cd1da2f085873d312a770e Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Wed, 18 Sep 2019 11:59:22 -0700 Subject: [PATCH 65/70] change 'Members' in UI to 'Children' (#168) --- islandora.links.action.yml | 2 +- .../config/install/views.view.manage_members.yml | 2 +- .../config/install/views.view.members.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/islandora.links.action.yml b/islandora.links.action.yml index 97b16e10..7f1f299d 100644 --- a/islandora.links.action.yml +++ b/islandora.links.action.yml @@ -6,6 +6,6 @@ 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 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: From 5453b10ad9c7ecdd5f59ddc38d03999fbfa24fda Mon Sep 17 00:00:00 2001 From: dannylamb Date: Wed, 18 Sep 2019 16:26:02 -0300 Subject: [PATCH 66/70] Text extraction (#163) --- .../field.storage.node.field_weight.yml | 23 ++ modules/islandora_text_extraction/LICENSE | 339 ++++++++++++++++++ modules/islandora_text_extraction/README.md | 44 +++ .../islandora_text_extraction.info.yml | 7 + .../islandora_text_extraction.install | 23 ++ .../islandora_text_extraction.module | 53 +++ .../Plugin/Action/GenerateOCRDerivative.php | 58 +++ .../Field/FieldFormatter/OcrTextFormatter.php | 82 +++++ .../tests/src/Functional/LoadTest.php | 46 +++ ...m_display.media.extracted_text.default.yml | 81 +++++ ...w_display.media.extracted_text.default.yml | 65 ++++ ...media.extracted_text.field_edited_text.yml | 20 ++ ....media.extracted_text.field_media_file.yml | 26 ++ ...ld.media.extracted_text.field_media_of.yml | 24 ++ ...d.media.extracted_text.field_media_use.yml | 28 ++ ...d.media.extracted_text.field_mime_type.yml | 20 ++ .../field.storage.media.field_edited_text.yml | 22 ++ ....content_settings.media.extracted_text.yml | 10 + .../install/media.type.extracted_text.yml | 12 + .../system.action.get_ocr_from_image.yml | 19 + ...dora_text_extraction_defaults.features.yml | 2 + ...slandora_text_extraction_defaults.info.yml | 17 + 22 files changed, 1021 insertions(+) create mode 100644 modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml create mode 100644 modules/islandora_text_extraction/LICENSE create mode 100644 modules/islandora_text_extraction/README.md create mode 100644 modules/islandora_text_extraction/islandora_text_extraction.info.yml create mode 100644 modules/islandora_text_extraction/islandora_text_extraction.install create mode 100644 modules/islandora_text_extraction/islandora_text_extraction.module create mode 100644 modules/islandora_text_extraction/src/Plugin/Action/GenerateOCRDerivative.php create mode 100644 modules/islandora_text_extraction/src/Plugin/Field/FieldFormatter/OcrTextFormatter.php create mode 100644 modules/islandora_text_extraction/tests/src/Functional/LoadTest.php create mode 100644 modules/islandora_text_extraction_defaults/config/install/core.entity_form_display.media.extracted_text.default.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/core.entity_view_display.media.extracted_text.default.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_edited_text.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_file.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_of.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_media_use.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/field.field.media.extracted_text.field_mime_type.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/field.storage.media.field_edited_text.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/language.content_settings.media.extracted_text.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/media.type.extracted_text.yml create mode 100644 modules/islandora_text_extraction_defaults/config/install/system.action.get_ocr_from_image.yml create mode 100644 modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.features.yml create mode 100644 modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml 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..dd75ab10 --- /dev/null +++ b/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + module: + - field_permissions + - node +third_party_settings: + field_permissions: + permission_type: public +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_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..ce145037 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/config/install/field.storage.media.field_edited_text.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + module: + - field_permissions + - media + - text +third_party_settings: + field_permissions: + permission_type: public +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..3f2b59e6 --- /dev/null +++ b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml @@ -0,0 +1,17 @@ +name: 'Islandora Text Extraction Defaults' +type: module +description: 'Default config for the Islandora Text Extraction module.' +core: 8.x +package: Islandora +dependencies: + - field + - field_permissions + - file + - islandora_core_feature + - islandora_text_extraction + - language + - media + - path + - system + - text + - user From fdd2b33b4d53a36d5547dd24dfd0f3e0647d993a Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Thu, 19 Sep 2019 08:11:03 -0700 Subject: [PATCH 67/70] remove field_permissions dependency (#169) --- .../config/install/field.storage.node.field_weight.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml b/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml index dd75ab10..97619cd2 100644 --- a/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml +++ b/modules/islandora_core_feature/config/install/field.storage.node.field_weight.yml @@ -2,11 +2,7 @@ langcode: en status: true dependencies: module: - - field_permissions - node -third_party_settings: - field_permissions: - permission_type: public id: node.field_weight field_name: field_weight entity_type: node From 1fbda7b4ce7b9a558f66c128f3cb6df6b54e43f5 Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Thu, 19 Sep 2019 09:28:36 -0700 Subject: [PATCH 68/70] Enable islandora_core_feature on Travis builds (#170) * Update .travis.yml * enable all the submodules * pull in upstream * remove unnecessary field_permissions dependency * simplify enabling submodules --- .travis.yml | 3 +++ .../config/install/field.storage.media.field_edited_text.yml | 4 ---- .../islandora_text_extraction_defaults.info.yml | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2743abda..685620f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,9 @@ 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/travis_scripts.sh 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 index ce145037..d8025161 100644 --- 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 @@ -2,12 +2,8 @@ langcode: en status: true dependencies: module: - - field_permissions - media - text -third_party_settings: - field_permissions: - permission_type: public id: media.field_edited_text field_name: field_edited_text entity_type: media diff --git a/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml index 3f2b59e6..5081bb27 100644 --- a/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml +++ b/modules/islandora_text_extraction_defaults/islandora_text_extraction_defaults.info.yml @@ -5,7 +5,6 @@ core: 8.x package: Islandora dependencies: - field - - field_permissions - file - islandora_core_feature - islandora_text_extraction From 93e55991a020deac79e6f21e0d40dccddf077b89 Mon Sep 17 00:00:00 2001 From: Alan Stanley Date: Fri, 20 Sep 2019 16:56:55 -0300 Subject: [PATCH 69/70] method check for remote videos (#162) * method check for remote videos * merged upstream changes * fixed syntax --- src/EventSubscriber/MediaLinkHeaderSubscriber.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EventSubscriber/MediaLinkHeaderSubscriber.php b/src/EventSubscriber/MediaLinkHeaderSubscriber.php index 644db872..5df879f0 100644 --- a/src/EventSubscriber/MediaLinkHeaderSubscriber.php +++ b/src/EventSubscriber/MediaLinkHeaderSubscriber.php @@ -71,7 +71,8 @@ class MediaLinkHeaderSubscriber extends LinkHeaderSubscriber implements EventSub $source_field = $type_configuration['source_field']; if (empty($source_field) || - !$media->hasField($source_field) + !$media->hasField($source_field) || + !method_exists($media->get($source_field), 'referencedEntities') ) { return $links; } From d45f94801739aa7b0bf22796084a93ffd5ed0b36 Mon Sep 17 00:00:00 2001 From: Seth Shaw Date: Fri, 20 Sep 2019 13:26:01 -0700 Subject: [PATCH 70/70] Integer-based weight drag-n-drop (Issue 1262) (#171) * add integer_weight_selector * test integer weight drag-n-drop * move reorder_children view to core --- islandora.links.action.yml | 6 + islandora.module | 44 +++ islandora.views.inc | 25 ++ .../install/views.view.reorder_children.yml | 271 ++++++++++++++++++ .../AdminViewsRouteSubscriber.php | 4 + .../views/field/IntegerWeightSelector.php | 101 +++++++ .../integer_weight_test_views.info.yml | 9 + .../views.view.test_integer_weight.yml | 251 ++++++++++++++++ .../IntegerWeightTest.php | 201 +++++++++++++ 9 files changed, 912 insertions(+) create mode 100644 islandora.views.inc create mode 100644 modules/islandora_core_feature/config/install/views.view.reorder_children.yml create mode 100644 src/Plugin/views/field/IntegerWeightSelector.php create mode 100644 tests/modules/integer_weight_test_views/integer_weight_test_views.info.yml create mode 100644 tests/modules/integer_weight_test_views/test_views/views.view.test_integer_weight.yml create mode 100644 tests/src/FunctionalJavascript/IntegerWeightTest.php diff --git a/islandora.links.action.yml b/islandora.links.action.yml index 7f1f299d..f8bf7d2b 100644 --- a/islandora.links.action.yml +++ b/islandora.links.action.yml @@ -9,3 +9,9 @@ islandora.add_member_to_node: 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 e39b4be4..dda56c8c 100644 --- a/islandora.module +++ b/islandora.module @@ -409,3 +409,47 @@ function islandora_entity_view(array &$build, EntityInterface $entity, EntityVie } } } + +/** + * 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.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/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/src/EventSubscriber/AdminViewsRouteSubscriber.php b/src/EventSubscriber/AdminViewsRouteSubscriber.php index 0ae1626c..64367673 100644 --- a/src/EventSubscriber/AdminViewsRouteSubscriber.php +++ b/src/EventSubscriber/AdminViewsRouteSubscriber.php @@ -24,6 +24,10 @@ class AdminViewsRouteSubscriber extends RouteSubscriberBase { $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/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/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/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."); + } + +}