From 10f68b951d96946f47fb10c03fa8c3252e81026b Mon Sep 17 00:00:00 2001 From: Bachir Soussi Chiadmi Date: Fri, 13 Apr 2018 19:23:02 +0200 Subject: [PATCH] started to activate translations ajax nav --- .../contrib/admin/redirect/LICENSE.txt | 339 ++ .../modules/contrib/admin/redirect/README.txt | 3 + .../contrib/admin/redirect/composer.json | 10 + .../config/install/redirect.settings.yml | 7 + .../system.action.redirect_delete_action.yml | 11 + .../config/install/views.view.redirect.yml | 593 ++++ ...age.content_settings.redirect.redirect.yml | 7 + .../config/schema/redirect.schema.yml | 31 + .../config/schema/redirect.views.schema.yml | 19 + .../admin/redirect/css/redirect.admin.css | 14 + .../redirect/migrations/d6_path_redirect.yml | 21 + .../redirect/migrations/d7_path_redirect.yml | 25 + .../config/install/redirect_404.settings.yml | 3 + .../install/views.view.redirect_404.yml | 523 ++++ .../config/schema/redirect_404.schema.yml | 14 + .../schema/redirect_404.views.schema.yml | 7 + .../redirect_404/redirect_404.info.yml | 14 + .../modules/redirect_404/redirect_404.install | 62 + .../redirect_404.links.action.yml | 5 + .../redirect_404/redirect_404.links.menu.yml | 6 + .../redirect_404/redirect_404.links.task.yml | 5 + .../modules/redirect_404/redirect_404.module | 96 + .../redirect_404/redirect_404.routing.yml | 15 + .../redirect_404/redirect_404.services.yml | 16 + .../redirect_404/redirect_404.views.inc | 99 + .../src/Controller/Fix404IgnoreController.php | 85 + .../EventSubscriber/Redirect404Subscriber.php | 130 + .../src/Form/RedirectFix404Form.php | 193 ++ .../src/Plugin/views/field/Language.php | 63 + .../views/field/Redirect404Operations.php | 123 + .../src/RedirectNotFoundStorageInterface.php | 53 + .../src/Render/Redirect404LogSuppressor.php | 65 + .../src/SqlRedirectNotFoundStorage.php | 162 + .../Tests/Fix404RedirectUILanguageTest.php | 202 ++ .../src/Tests/Fix404RedirectUITest.php | 185 ++ .../Tests/Redirect404LogSuppressorTest.php | 80 + .../src/Tests/Redirect404TestBase.php | 144 + .../src/Kernel/Fix404RedirectCronJobTest.php | 192 ++ .../Unit/SqlRedirectNotFoundStorageTest.php | 78 + .../install/redirect_domain.domains.yml | 1 + .../config/schema/redirect_domain.schema.yml | 18 + .../redirect_domain/redirect_domain.info.yml | 13 + .../redirect_domain.links.task.yml | 5 + .../redirect_domain/redirect_domain.module | 33 + .../redirect_domain.routing.yml | 7 + .../redirect_domain.services.yml | 6 + .../DomainRedirectRequestSubscriber.php | 133 + .../src/Form/RedirectDomainForm.php | 165 + .../src/Tests/RedirectDomainUITest.php | 64 + .../DomainRedirectRequestSubscriberTest.php | 144 + .../contrib/admin/redirect/redirect.api.php | 142 + .../contrib/admin/redirect/redirect.drush.inc | 72 + .../admin/redirect/redirect.generate.inc | 194 ++ .../contrib/admin/redirect/redirect.info.yml | 15 + .../contrib/admin/redirect/redirect.install | 224 ++ .../admin/redirect/redirect.libraries.yml | 5 + .../admin/redirect/redirect.links.action.yml | 5 + .../admin/redirect/redirect.links.menu.yml | 26 + .../admin/redirect/redirect.links.task.yml | 10 + .../contrib/admin/redirect/redirect.module | 497 +++ .../admin/redirect/redirect.permissions.yml | 4 + .../admin/redirect/redirect.routing.yml | 58 + .../admin/redirect/redirect.services.yml | 30 + .../admin/redirect/src/Entity/Redirect.php | 344 +++ .../RedirectRequestSubscriber.php | 201 ++ .../RedirectSettingsCacheTag.php | 54 + .../RouteNormalizerRequestSubscriber.php | 149 + .../src/Exception/RedirectLoopException.php | 24 + .../redirect/src/Form/RedirectDeleteForm.php | 41 + .../src/Form/RedirectDeleteMultipleForm.php | 140 + .../admin/redirect/src/Form/RedirectForm.php | 152 + .../src/Form/RedirectSettingsForm.php | 93 + .../src/Plugin/Action/DeleteRedirect.php | 92 + .../RedirectSourceFormatter.php | 36 + .../Field/FieldType/RedirectSourceItem.php | 112 + .../FieldWidget/RedirectSourceWidget.php | 117 + .../Constraint/SourceLinkTypeConstraint.php | 92 + .../Plugin/migrate/process/PathRedirect.php | 42 + .../migrate/process/d7/PathRedirect.php | 54 + .../process/d7/RedirectSourceQuery.php | 41 + .../Plugin/migrate/source/PathRedirect.php | 62 + .../Plugin/migrate/source/d7/PathRedirect.php | 81 + .../admin/redirect/src/RedirectChecker.php | 98 + .../admin/redirect/src/RedirectRepository.php | 178 ++ .../redirect/src/RedirectStorageSchema.php | 31 + .../redirect/src/Routing/RouteSubscriber.php | 28 + .../src/Tests/AssertRedirectTrait.php | 53 + .../redirect/src/Tests/GlobalRedirectTest.php | 276 ++ .../src/Tests/RedirectUILanguageTest.php | 94 + .../redirect/src/Tests/RedirectUITest.php | 437 +++ .../admin/redirect/tests/fixtures/drupal6.php | 187 ++ .../admin/redirect/tests/fixtures/drupal7.php | 240 ++ .../Kernel/Migrate/d6/PathRedirectTest.php | 50 + .../Kernel/Migrate/d7/PathRedirectTest.php | 68 + .../tests/src/Kernel/RedirectAPITest.php | 292 ++ .../src/Unit/Migrate/d7/PathRedirectTest.php | 54 + .../tests/src/Unit/RedirectCheckerTest.php | 155 + .../Unit/RedirectRequestSubscriberTest.php | 282 ++ .../RouteNormalizerRequestSubscriberTest.php | 225 ++ .../views/translation_views/LICENSE.txt | 339 ++ .../contrib/views/translation_views/README.md | 65 + .../views.view.content_translations.yml | 952 ++++++ .../schema/translation_views.views.schema.yml | 6 + .../translation_views/src/EntityInfo.php | 78 + .../views/field/TranslationCountField.php | 53 + .../field/TranslationOperationsField.php | 241 ++ ...onSourceLangcodeEqualsRowLangcodeField.php | 49 + .../Plugin/views/field/TranslationStatus.php | 95 + .../field/TranslationTargetLanguageField.php | 84 + .../views/filter/TranslationCountFilter.php | 62 + ...nSourceLangcodeEqualsRowLangcodeFilter.php | 31 + .../views/filter/TranslationStatusFilter.php | 134 + .../TranslationTargetLanguageFilter.php | 151 + .../views/join/TranslationLanguageJoin.php | 68 + .../src/TranslationCountTrait.php | 58 + .../src/TranslationViewsFieldDefinitions.php | 147 + .../src/TranslationViewsTargetLanguage.php | 41 + .../translation_views.info.yml | 14 + .../translation_views.module | 114 + .../translation_views.views.inc | 108 + .../translation_views.views_execution.inc | 36 + .../modules/figli/edlp_ajax/edlp_ajax.module | 3 + .../assets/dist/scripts/corpus.min.js | 1043 ++++++- .../assets/dist/scripts/physics.min.js | 1189 +++++++- .../assets/dist/styles/corpus.min.css | 72 +- .../assets/dist/scripts/edlp_search.min.js | 111 +- .../assets/dist/scripts/edlp_studio.min.js | 416 ++- .../assets/dist/scripts/history.min.js | 15 +- .../edlptheme/assets/dist/scripts/main.min.js | 1224 +++++++- .../edlptheme/assets/dist/styles/app.min.css | 2713 ++++++++++++++++- .../edlptheme/assets/scripts/history.js | 5 +- .../custom/edlptheme/assets/scripts/main.js | 6 + .../themes/custom/edlptheme/edlptheme.theme | 8 + .../links--language-block.html.twig | 3 +- ...u_link_content.menu_link_content.title.yml | 2 +- sites/default/config/sync/core.extension.yml | 2 + ...gs.menu_link_content.menu_link_content.yml | 6 +- ...age.content_settings.redirect.redirect.yml | 13 + .../config/sync/language.negotiation.yml | 2 +- .../system.action.redirect_delete_action.yml | 1 + .../en/views.view.content_translations.yml | 59 + .../sync/language/en/views.view.redirect.yml | 68 + sites/default/config/sync/locale.settings.yml | 2 +- .../default/config/sync/redirect.settings.yml | 10 + .../system.action.redirect_delete_action.yml | 16 + sites/default/config/sync/user.role.admin.yml | 1 + sites/default/config/sync/user.role.root.yml | 1 + .../config/sync/views.view.archive.yml | 2 +- .../config/sync/views.view.content.yml | 249 +- .../sync/views.view.content_translations.yml | 955 ++++++ .../config/sync/views.view.frontpage.yml | 2 +- .../config/sync/views.view.glossary.yml | 2 +- .../sync/views.view.maillog_overview.yml | 2 +- .../config/sync/views.view.redirect.yml | 600 ++++ .../config/sync/views.view.taxonomy_term.yml | 2 +- .../sync/views.view.user_admin_people.yml | 2 +- 156 files changed, 21811 insertions(+), 168 deletions(-) create mode 100644 sites/all/modules/contrib/admin/redirect/LICENSE.txt create mode 100644 sites/all/modules/contrib/admin/redirect/README.txt create mode 100644 sites/all/modules/contrib/admin/redirect/composer.json create mode 100644 sites/all/modules/contrib/admin/redirect/config/install/redirect.settings.yml create mode 100644 sites/all/modules/contrib/admin/redirect/config/install/system.action.redirect_delete_action.yml create mode 100644 sites/all/modules/contrib/admin/redirect/config/install/views.view.redirect.yml create mode 100644 sites/all/modules/contrib/admin/redirect/config/optional/language.content_settings.redirect.redirect.yml create mode 100644 sites/all/modules/contrib/admin/redirect/config/schema/redirect.schema.yml create mode 100644 sites/all/modules/contrib/admin/redirect/config/schema/redirect.views.schema.yml create mode 100644 sites/all/modules/contrib/admin/redirect/css/redirect.admin.css create mode 100644 sites/all/modules/contrib/admin/redirect/migrations/d6_path_redirect.yml create mode 100644 sites/all/modules/contrib/admin/redirect/migrations/d7_path_redirect.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/redirect_404.settings.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/views.view.redirect_404.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.schema.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.views.schema.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.info.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.install create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.action.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.menu.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.task.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.module create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.routing.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.services.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.views.inc create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Controller/Fix404IgnoreController.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Language.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/RedirectNotFoundStorageInterface.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Render/Redirect404LogSuppressor.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/SqlRedirectNotFoundStorage.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404LogSuppressorTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404TestBase.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/install/redirect_domain.domains.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/schema/redirect_domain.schema.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.info.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.links.task.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.module create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.routing.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.services.yml create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/EventSubscriber/DomainRedirectRequestSubscriber.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Form/RedirectDomainForm.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Tests/RedirectDomainUITest.php create mode 100644 sites/all/modules/contrib/admin/redirect/modules/redirect_domain/tests/src/Unit/DomainRedirectRequestSubscriberTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.api.php create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.drush.inc create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.generate.inc create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.info.yml create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.install create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.libraries.yml create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.links.action.yml create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.links.menu.yml create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.links.task.yml create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.module create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.permissions.yml create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.routing.yml create mode 100644 sites/all/modules/contrib/admin/redirect/redirect.services.yml create mode 100644 sites/all/modules/contrib/admin/redirect/src/Entity/Redirect.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectRequestSubscriber.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectSettingsCacheTag.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RouteNormalizerRequestSubscriber.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Exception/RedirectLoopException.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteForm.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteMultipleForm.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Form/RedirectForm.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Form/RedirectSettingsForm.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/Action/DeleteRedirect.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldFormatter/RedirectSourceFormatter.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldType/RedirectSourceItem.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldWidget/RedirectSourceWidget.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/Validation/Constraint/SourceLinkTypeConstraint.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/process/PathRedirect.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/process/d7/PathRedirect.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/process/d7/RedirectSourceQuery.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/source/PathRedirect.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/source/d7/PathRedirect.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/RedirectChecker.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/RedirectRepository.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/RedirectStorageSchema.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Routing/RouteSubscriber.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Tests/AssertRedirectTrait.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Tests/GlobalRedirectTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUILanguageTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUITest.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal6.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal7.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d6/PathRedirectTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d7/PathRedirectTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/src/Kernel/RedirectAPITest.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/src/Unit/Migrate/d7/PathRedirectTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectCheckerTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectRequestSubscriberTest.php create mode 100644 sites/all/modules/contrib/admin/redirect/tests/src/Unit/RouteNormalizerRequestSubscriberTest.php create mode 100644 sites/all/modules/contrib/views/translation_views/LICENSE.txt create mode 100644 sites/all/modules/contrib/views/translation_views/README.md create mode 100644 sites/all/modules/contrib/views/translation_views/config/install/views.view.content_translations.yml create mode 100644 sites/all/modules/contrib/views/translation_views/config/schema/translation_views.views.schema.yml create mode 100644 sites/all/modules/contrib/views/translation_views/src/EntityInfo.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationCountField.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationOperationsField.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationSourceLangcodeEqualsRowLangcodeField.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationStatus.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationTargetLanguageField.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationCountFilter.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationSourceLangcodeEqualsRowLangcodeFilter.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationStatusFilter.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationTargetLanguageFilter.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/Plugin/views/join/TranslationLanguageJoin.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/TranslationCountTrait.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/TranslationViewsFieldDefinitions.php create mode 100644 sites/all/modules/contrib/views/translation_views/src/TranslationViewsTargetLanguage.php create mode 100644 sites/all/modules/contrib/views/translation_views/translation_views.info.yml create mode 100644 sites/all/modules/contrib/views/translation_views/translation_views.module create mode 100644 sites/all/modules/contrib/views/translation_views/translation_views.views.inc create mode 100644 sites/all/modules/contrib/views/translation_views/translation_views.views_execution.inc create mode 100644 sites/default/config/sync/language.content_settings.redirect.redirect.yml create mode 100644 sites/default/config/sync/language/en/system.action.redirect_delete_action.yml create mode 100644 sites/default/config/sync/language/en/views.view.content_translations.yml create mode 100644 sites/default/config/sync/language/en/views.view.redirect.yml create mode 100644 sites/default/config/sync/redirect.settings.yml create mode 100644 sites/default/config/sync/system.action.redirect_delete_action.yml create mode 100644 sites/default/config/sync/views.view.content_translations.yml create mode 100644 sites/default/config/sync/views.view.redirect.yml diff --git a/sites/all/modules/contrib/admin/redirect/LICENSE.txt b/sites/all/modules/contrib/admin/redirect/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/LICENSE.txt @@ -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. diff --git a/sites/all/modules/contrib/admin/redirect/README.txt b/sites/all/modules/contrib/admin/redirect/README.txt new file mode 100644 index 000000000..3ea2f179a --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/README.txt @@ -0,0 +1,3 @@ + +This is the new module home for a unified redirection API (also replaces +path_redirect and globalredirect). diff --git a/sites/all/modules/contrib/admin/redirect/composer.json b/sites/all/modules/contrib/admin/redirect/composer.json new file mode 100644 index 000000000..b4f147895 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/composer.json @@ -0,0 +1,10 @@ +{ + "name": "drupal/redirect", + "description": "Allows users to redirect from old URLs to new URLs.", + "type": "drupal-module", + "license": "GPL-2.0+", + "minimum-stability": "dev", + "require": { + "drupal/core": "~8" + } +} diff --git a/sites/all/modules/contrib/admin/redirect/config/install/redirect.settings.yml b/sites/all/modules/contrib/admin/redirect/config/install/redirect.settings.yml new file mode 100644 index 000000000..70a96888d --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/config/install/redirect.settings.yml @@ -0,0 +1,7 @@ +auto_redirect: true +default_status_code: 301 +passthrough_querystring: true +warning: false +ignore_admin_path: false +access_check: false +route_normalizer_enabled: true diff --git a/sites/all/modules/contrib/admin/redirect/config/install/system.action.redirect_delete_action.yml b/sites/all/modules/contrib/admin/redirect/config/install/system.action.redirect_delete_action.yml new file mode 100644 index 000000000..dccf94ced --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/config/install/system.action.redirect_delete_action.yml @@ -0,0 +1,11 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - redirect +id: redirect_delete_action +label: 'Delete redirect' +type: redirect +plugin: redirect_delete_action +configuration: { } diff --git a/sites/all/modules/contrib/admin/redirect/config/install/views.view.redirect.yml b/sites/all/modules/contrib/admin/redirect/config/install/views.view.redirect.yml new file mode 100644 index 000000000..8a2829af6 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/config/install/views.view.redirect.yml @@ -0,0 +1,593 @@ +langcode: en +status: true +dependencies: + module: + - link + - redirect + - user +id: redirect +label: Redirect +module: views +description: 'List of redirects' +tag: '' +base_table: redirect +base_field: rid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'administer redirects' + 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: 50 + offset: 0 + id: 0 + total_pages: null + tags: + previous: '‹ previous' + next: 'next ›' + first: '« first' + last: 'last »' + 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 + quantity: 9 + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + redirect_source__path: redirect_source__path + redirect_redirect__uri: redirect_redirect__uri + status_code: status_code + language: language + created: created + operations: operations + info: + redirect_source__path: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + redirect_redirect__uri: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + status_code: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + language: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + created: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: created + empty_table: false + row: + type: fields + fields: + redirect_bulk_form: + id: redirect_bulk_form + table: redirect + field: redirect_bulk_form + 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 + action_title: 'With selection' + include_exclude: exclude + selected_actions: { } + entity_type: redirect + plugin_id: redirect_bulk_form + redirect_source__path: + id: redirect_source__path + table: redirect + field: redirect_source__path + relationship: none + group_type: group + admin_label: '' + label: From + 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 + click_sort_column: path + type: redirect_source + settings: { } + group_column: '' + 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 + entity_type: redirect + entity_field: redirect_source + plugin_id: field + redirect_redirect__uri: + id: redirect_redirect__uri + table: redirect + field: redirect_redirect__uri + entity_type: redirect + entity_field: redirect_redirect + plugin_id: field + status_code: + id: status_code + table: redirect + field: status_code + entity_type: redirect + entity_field: status_code + plugin_id: field + language: + id: language + table: redirect + field: language + entity_type: redirect + entity_field: language + plugin_id: field + created: + id: created + table: redirect + field: created + relationship: none + group_type: group + admin_label: '' + label: Created + 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 + date_format: fallback + custom_date_format: '' + timezone: '' + entity_type: redirect + entity_field: created + plugin_id: date + operations: + id: operations + table: redirect + field: operations + entity_type: redirect + plugin_id: entity_operations + filters: + redirect_source__path: + id: redirect_source__path + table: redirect + field: redirect_source__path + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: redirect_source__path_op + label: From + description: '' + use_operator: false + operator: redirect_source__path_op + identifier: redirect_source__path + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: redirect + entity_field: redirect_source + plugin_id: string + redirect_redirect__uri: + id: redirect_redirect__uri + table: redirect + field: redirect_redirect__uri + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: redirect_redirect__uri_op + label: To + description: '' + use_operator: false + operator: redirect_redirect__uri_op + identifier: redirect_redirect__uri + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: redirect + entity_field: redirect_redirect + plugin_id: string + status_code: + id: status_code + table: redirect + field: status_code + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: + min: '' + max: '' + value: '' + group: 1 + exposed: true + expose: + operator_id: status_code_op + label: 'Status code' + description: '' + use_operator: false + operator: status_code_op + identifier: status_code + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: true + group_info: + label: 'Status code' + description: '' + identifier: status_code + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: + 1: + title: '300 Multiple Choices' + operator: '=' + value: + value: '300' + min: '' + max: '' + 2: + title: '301 Moved Permanently' + operator: '=' + value: + value: '301' + min: '' + max: '' + 3: + title: '302 Found' + operator: '=' + value: + value: '302' + min: '' + max: '' + 4: + title: '303 See Other' + operator: '=' + value: + value: '303' + min: '' + max: '' + 5: + title: '304 Not Modified' + operator: '=' + value: + value: '304' + min: '' + max: '' + 6: + title: '305 Use Proxy' + operator: '=' + value: + value: '305' + min: '' + max: '' + 7: + title: '307 Temporary Redirect' + operator: '=' + value: + value: '307' + min: '' + max: '' + entity_type: redirect + entity_field: status_code + plugin_id: numeric + language: + id: language + table: redirect + field: language + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: language_op + label: 'Original language' + description: '' + use_operator: false + operator: language_op + identifier: language + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: redirect + entity_field: language + plugin_id: language + sorts: { } + title: Redirect + header: { } + footer: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: 'There is no redirect yet.' + plugin_id: text_custom + relationships: { } + arguments: { } + display_extenders: { } + filter_groups: + operator: AND + groups: + 1: AND + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + cacheable: false + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: admin/config/search/redirect + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + cacheable: false diff --git a/sites/all/modules/contrib/admin/redirect/config/optional/language.content_settings.redirect.redirect.yml b/sites/all/modules/contrib/admin/redirect/config/optional/language.content_settings.redirect.redirect.yml new file mode 100644 index 000000000..ce3bd07d2 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/config/optional/language.content_settings.redirect.redirect.yml @@ -0,0 +1,7 @@ +status: true +dependencies: { } +id: redirect.redirect +target_entity_type_id: redirect +target_bundle: redirect +default_langcode: und +language_alterable: true diff --git a/sites/all/modules/contrib/admin/redirect/config/schema/redirect.schema.yml b/sites/all/modules/contrib/admin/redirect/config/schema/redirect.schema.yml new file mode 100644 index 000000000..bd9ccd5d6 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/config/schema/redirect.schema.yml @@ -0,0 +1,31 @@ +# Schema for the configuration files of the Redirect module. + +redirect.settings: + type: config_entity + label: 'Redirect settings' + mapping: + auto_redirect: + type: boolean + label: 'Automatically create redirects when URL aliases are changed.' + default_status_code: + type: integer + label: 'Default redirect status' + passthrough_querystring: + type: boolean + label: 'Retain query string through redirect.' + warning: + type: boolean + label: 'Display a warning message to users when they are redirected.' + ignore_admin_path: + type: boolean + label: 'Allow redirections on admin paths.' + access_check: + type: boolean + label: 'Menu Access Checking' + route_normalizer_enabled: + type: boolean + label: 'Enforce clean and canonical URLs.' + +action.configuration.redirect_delete_action: + type: action_configuration_default + label: 'Delete redirect configuration' diff --git a/sites/all/modules/contrib/admin/redirect/config/schema/redirect.views.schema.yml b/sites/all/modules/contrib/admin/redirect/config/schema/redirect.views.schema.yml new file mode 100644 index 000000000..59a565ce4 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/config/schema/redirect.views.schema.yml @@ -0,0 +1,19 @@ +# Schema for the views plugins of the Redirect module. + +views.field.redirect_type: + type: views.field.redirect + label: 'Redirect type' + mapping: + machine_name: + type: string + label: 'Output machine name' + +# Schema for the formatter of Redirect Source +field.formatter.settings.redirect_source: + type: mapping + label: 'Redirect Source formatter' + mapping: { } + +views.field.redirect_bulk_form: + type: views_field_bulk_form + label: 'Redirect bulk form' diff --git a/sites/all/modules/contrib/admin/redirect/css/redirect.admin.css b/sites/all/modules/contrib/admin/redirect/css/redirect.admin.css new file mode 100644 index 000000000..f613931ed --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/css/redirect.admin.css @@ -0,0 +1,14 @@ +.redirect-table { + table-layout: fixed; +} + +.redirect-table__operations { + width: 30%; +} + +.redirect-table__path { + width: 70%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/sites/all/modules/contrib/admin/redirect/migrations/d6_path_redirect.yml b/sites/all/modules/contrib/admin/redirect/migrations/d6_path_redirect.yml new file mode 100644 index 000000000..1f3ddef96 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/migrations/d6_path_redirect.yml @@ -0,0 +1,21 @@ +id: d6_path_redirect +label: Path Redirect +migration_tags: + - Drupal 6 +source: + plugin: d6_path_redirect +process: + rid: rid + redirect_source: source + redirect_redirect: + plugin: d6_path_redirect + source: + - redirect + - query + language: + plugin: default_value + source: language + default_value: und + status_code: type +destination: + plugin: entity:redirect \ No newline at end of file diff --git a/sites/all/modules/contrib/admin/redirect/migrations/d7_path_redirect.yml b/sites/all/modules/contrib/admin/redirect/migrations/d7_path_redirect.yml new file mode 100644 index 000000000..3ee9a287d --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/migrations/d7_path_redirect.yml @@ -0,0 +1,25 @@ +id: d7_path_redirect +label: Path Redirect +migration_tags: + - Drupal 7 +source: + plugin: d7_path_redirect +process: + rid: rid + uid: uid + redirect_source/path: source + redirect_source/query: + plugin: d7_redirect_source_query + source: source_options + redirect_redirect/uri: + plugin: d7_path_redirect + source: + - redirect + - redirect_options + language: + plugin: default_value + source: language + default_value: und + status_code: status_code +destination: + plugin: entity:redirect diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/redirect_404.settings.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/redirect_404.settings.yml new file mode 100644 index 000000000..030db9f0a --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/redirect_404.settings.yml @@ -0,0 +1,3 @@ +row_limit: 10000 +pages: '' +suppress_404: FALSE diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/views.view.redirect_404.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/views.view.redirect_404.yml new file mode 100644 index 000000000..75777dee4 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/install/views.view.redirect_404.yml @@ -0,0 +1,523 @@ +langcode: en +status: true +dependencies: + module: + - redirect_404 + - user +id: redirect_404 +label: 'Redirect 404' +module: views +description: '' +tag: '' +base_table: redirect_404 +base_field: '' +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'administer redirects' + cache: + type: none + 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: Filter + reset_button: true + 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 + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + path: path + count: count + timestamp: timestamp + info: + path: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + count: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + timestamp: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + default: count + empty_table: false + row: + type: fields + fields: + path: + table: redirect_404 + field: path + id: path + entity_type: null + entity_field: null + plugin_id: standard + relationship: none + group_type: group + admin_label: '' + label: Path + 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 + count: + id: count + table: redirect_404 + field: count + relationship: none + group_type: group + admin_label: '' + label: Count + 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 + format: unserialized + key: '' + plugin_id: serialized + timestamp: + id: timestamp + table: redirect_404 + field: timestamp + relationship: none + group_type: group + admin_label: '' + label: 'Last accessed' + 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 + date_format: short + custom_date_format: '' + timezone: '' + plugin_id: date + langcode: + id: langcode + table: redirect_404 + field: langcode + relationship: none + group_type: group + admin_label: '' + label: Language + 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 + plugin_id: standard + redirect_404_operations: + id: redirect_404_operations + table: redirect_404 + field: redirect_404_operations + relationship: none + group_type: group + admin_label: '' + label: Operations + 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 + plugin_id: redirect_404_operations + filters: + path: + id: path + table: redirect_404 + field: path + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: path_op + label: Path + description: '' + use_operator: false + operator: path_op + identifier: path + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + 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 + langcode: + id: langcode + table: redirect_404 + field: langcode + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: langcode_op + label: Language + description: '' + use_operator: false + operator: langcode_op + identifier: langcode + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + 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: in_operator + resolved: + id: resolved + table: redirect_404 + field: resolved + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '0' + group: 1 + exposed: false + expose: + operator_id: '' + label: Resolved + description: '' + use_operator: false + operator: resolved_op + identifier: resolved + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + 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: boolean + sorts: { } + title: 'Fix 404 pages' + header: { } + footer: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: 'There are no 404 errors to fix.' + plugin_id: text_custom + relationships: { } + arguments: { } + display_extenders: { } + filter_groups: + operator: AND + groups: + 1: AND + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: admin/config/search/redirect/404 + display_description: 'Lists 404 error paths with no redirect assigned yet.' + enabled: true + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.schema.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.schema.yml new file mode 100644 index 000000000..3aaeb9d14 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.schema.yml @@ -0,0 +1,14 @@ +# Schema for the configuration files of the redirect_404 module. + +redirect_404.settings: + type: config_object + label: '404 error database logging settings.' + mapping: + row_limit: + type: integer + label: '404 error database logs to keep.' + pages: + type: string + suppress_404: + type: boolean + label: "Whether to log 'page not found' messages to the standard log or not." diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.views.schema.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.views.schema.yml new file mode 100644 index 000000000..5798b983e --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/config/schema/redirect_404.views.schema.yml @@ -0,0 +1,7 @@ +views.field.redirect_404_operations: + type: views_field + label: 'Redirect 404 operations' + mapping: + text: + type: label + label: 'Redirect 404 operations' diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.info.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.info.yml new file mode 100644 index 000000000..96819d27b --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.info.yml @@ -0,0 +1,14 @@ +name: 'Redirect 404' +type: module +description: 'Logs 404 errors and allows users to create redirects for often requested but missing pages.' +# core: 8.x + +dependencies: + - redirect + - views + +# Information added by Drupal.org packaging script on 2018-03-12 +version: '8.x-1.1' +core: '8.x' +project: 'redirect' +datestamp: 1520897884 diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.install b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.install new file mode 100644 index 000000000..09190e7cf --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.install @@ -0,0 +1,62 @@ + 'Stores 404 requests.', + 'fields' => [ + 'path' => [ + 'description' => 'The path of the request.', + 'type' => 'varchar', + 'length' => SqlRedirectNotFoundStorage::MAX_PATH_LENGTH, + 'not null' => TRUE, + ], + 'langcode' => [ + 'description' => 'The language of this request.', + 'type' => 'varchar_ascii', + 'length' => 12, + 'not null' => TRUE, + 'default' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + 'count' => [ + 'description' => 'The number of requests with that path and language.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + 'timestamp' => [ + 'description' => 'The timestamp of the last request with that path and language.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + 'resolved' => [ + 'description' => 'Boolean indicating whether or not this path has a redirect assigned.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ], + ], + 'primary key' => ['path', 'langcode'], + ]; + return $schema; +} + +/** + * Remove relevancy field from the redirect_404 table. + */ +function redirect_404_update_8101() { + \Drupal::database()->schema()->dropField('redirect_404', 'relevancy'); +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.action.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.action.yml new file mode 100644 index 000000000..24c721adb --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.action.yml @@ -0,0 +1,5 @@ +redirect_404.goto_fix_404: + route_name: redirect_404.fix_404 + title: 'Fix 404 pages with URL redirects' + appears_on: + - 'dblog.page_not_found' diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.menu.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.menu.yml new file mode 100644 index 000000000..28622669f --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.menu.yml @@ -0,0 +1,6 @@ +redirect_404.fix_404: + title: 'Fix 404 pages' + parent: redirect.list + route_name: redirect_404.fix_404 + description: 'Add redirects for 404 pages.' + menu_name: admin diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.task.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.task.yml new file mode 100644 index 000000000..955161a11 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.links.task.yml @@ -0,0 +1,5 @@ +redirect_404.fix_404: + route_name: redirect_404.fix_404 + base_route: redirect.list + title: Fix 404 pages + weight: 30 diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.module b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.module new file mode 100644 index 000000000..b90d166e1 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.module @@ -0,0 +1,96 @@ +purgeOldRequests(); +} + +/** + * Implements hook_form_FORM_ID_alter() for system_logging_settings(). + */ +function redirect_404_form_redirect_settings_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $config = \Drupal::configFactory()->getEditable('redirect_404.settings'); + + $row_limits = [100, 1000, 10000, 100000, 1000000]; + $form['row_limit'] = [ + '#type' => 'select', + '#title' => t('404 error database logs to keep'), + '#default_value' => $config->get('row_limit'), + '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits), + '#description' => t('The maximum number of 404 error logs to keep in the database log. Requires a cron maintenance task.', [':cron' => Url::fromRoute('system.status')->toString()]) + ]; + + $ignored_pages = $config->get('pages'); + // Add a new path to be ignored, if there is an ignore argument in the query. + if ($path_to_ignore = \Drupal::request()->query->get('ignore')) { + $ignored_pages .= "\n" . $path_to_ignore; + } + + $form['ignore_pages'] = [ + '#type' => 'textarea', + '#title' => t('Pages to ignore'), + '#default_value' => $ignored_pages, + '#description' => t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. An example path is %user-wildcard for every user page. %front is the front page.", [ + '%user-wildcard' => '/user/*', + '%front' => '', + ]), + ]; + + $form['suppress_404'] = [ + '#type' => 'checkbox', + '#title' => t("Suppress 'page not found' log messages"), + '#default_value' => $config->get('suppress_404'), + '#description' => t("Prevents logging 'page not found' events. Can be safely enabled when redirect_404 module is used, which stores them separately, nothing else relies on those messages."), + ]; + + $form['#submit'][] = 'redirect_404_logging_settings_submit'; +} + +/** + * Form submission handler for system_logging_settings(). + * + * @see redirect_404_form_redirect_settings_form_alter() + */ +function redirect_404_logging_settings_submit($form, FormStateInterface $form_state) { + // Make sure to store the 'pages to ignore' with the leading slash. + $ignore_pages = explode(PHP_EOL, $form_state->getValue('ignore_pages')); + $pages = ''; + foreach ($ignore_pages as $page) { + if (!empty($page)) { + $pages .= '/' . ltrim($page, '/') . "\n"; + } + } + + \Drupal::configFactory() + ->getEditable('redirect_404.settings') + ->set('row_limit', $form_state->getValue('row_limit')) + ->set('pages', $pages) + ->set('suppress_404', $form_state->getValue('suppress_404')) + ->save(); +} + +/** + * Implements hook_ENTITY_TYPE_presave() for redirect entities. + */ +function redirect_404_redirect_presave(Redirect $redirect) { + $path = $redirect->getSourcePathWithQuery(); + $langcode = $redirect->get('language')->value; + + // Mark a potentially existing log entry for this path as resolved. + \Drupal::service('redirect.not_found_storage')->resolveLogRequest($path, $langcode); +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.routing.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.routing.yml new file mode 100644 index 000000000..15146cba9 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.routing.yml @@ -0,0 +1,15 @@ +redirect_404.fix_404: + path: '/admin/config/search/redirect/404' + defaults: + _title: 'Fix 404 pages' + _form: '\Drupal\redirect_404\Form\RedirectFix404Form' + requirements: + _permission: 'administer redirects' + +redirect_404.ignore_404: + path: '/admin/config/search/redirect/404/ignore' + defaults: + _controller: '\Drupal\redirect_404\Controller\Fix404IgnoreController::ignorePath' + requirements: + _permission: 'administer redirect settings' + _csrf_token: 'TRUE' diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.services.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.services.yml new file mode 100644 index 000000000..8221015ab --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.services.yml @@ -0,0 +1,16 @@ +services: + redirect.404_subscriber: + class: Drupal\redirect_404\EventSubscriber\Redirect404Subscriber + arguments: ['@path.current', '@path.matcher', '@request_stack', '@language_manager', '@redirect.not_found_storage', '@config.factory'] + tags: + - { name: event_subscriber } + redirect.not_found_storage: + class: Drupal\redirect_404\SqlRedirectNotFoundStorage + arguments: ['@database', '@config.factory'] + tags: + - { name: backend_overridable } + logger.redirect_404: + public: false + class: \Drupal\redirect_404\Render\Redirect404LogSuppressor + decorates: logger.factory + arguments: ['@logger.redirect_404.inner', '@config.factory'] diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.views.inc b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.views.inc new file mode 100644 index 000000000..bc18f93e1 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/redirect_404.views.inc @@ -0,0 +1,99 @@ + '', + 'title' => t('Fix 404 pages'), + 'help' => t('Overview for 404 error paths with no redirect assigned yet.'), + ]; + + $data['redirect_404']['path'] = [ + 'title' => t('Path'), + 'help' => t('The path of the request.'), + 'field' => [ + 'id' => 'standard', + ], + 'filter' => [ + 'id' => 'string', + ], + ]; + + $data['redirect_404']['langcode'] = [ + 'title' => t('Language'), + 'help' => t('The language of this request.'), + 'field' => [ + 'id' => 'redirect_404_langcode', + ], + 'filter' => [ + 'id' => 'language', + ], + ]; + + $data['redirect_404']['count'] = [ + 'title' => t('Count'), + 'help' => t('The number of requests with that path and language.'), + 'field' => [ + 'id' => 'numeric', + 'click sortable' => TRUE, + ], + 'filter' => [ + 'id' => 'numeric', + ], + ]; + + $data['redirect_404']['timestamp'] = [ + 'title' => t('Timestamp'), + 'help' => t('The timestamp of the last request with that path and language.'), + 'field' => [ + 'id' => 'date', + 'click sortable' => TRUE, + ], + 'filter' => [ + 'id' => 'date', + ], + ]; + + $data['redirect_404']['resolved'] = [ + 'title' => t('Resolved'), + 'help' => t('Whether or not this path has a redirect assigned.'), + 'field' => [ + 'id' => 'boolean', + ], + 'filter' => [ + 'id' => 'boolean', + 'label' => t('Resolved'), + 'use_equal' => TRUE, + ], + ]; + + $data['redirect_404']['redirect_404_operations'] = [ + 'title' => t('Operations'), + 'help' => t('Provide operation buttons to handle the 404 path.'), + 'field' => [ + 'id' => 'redirect_404_operations', + 'additional fields' => ['path', 'langcode'], + 'real field' => 'path', + ], + ]; + + return $data; +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Controller/Fix404IgnoreController.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Controller/Fix404IgnoreController.php new file mode 100644 index 000000000..a00331188 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Controller/Fix404IgnoreController.php @@ -0,0 +1,85 @@ +configuration = $config_factory; + $this->redirectStorage = $redirect_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('redirect.not_found_storage') + ); + } + + /** + * Adds path into the ignored list. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function ignorePath(Request $request) { + $ignored_paths = $this->config('redirect_404.settings')->get('pages'); + $path = $request->query->get('path'); + $langcode = $request->query->get('langcode'); + + if (empty($ignored_paths) || !strpos($path, $ignored_paths)) { + $this->redirectStorage->resolveLogRequest($path, $langcode); + + drupal_set_message($this->t('Resolved the path %path in the database. Please check the ignored list and save the settings.', [ + '%path' => $path, + ])); + } + + $options = [ + 'query' => [ + 'ignore' => $path, + 'destination' => Url::fromRoute('redirect_404.fix_404')->getInternalPath(), + ], + ]; + + return $this->redirect('redirect.settings', [], $options); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php new file mode 100644 index 000000000..7a54e86db --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php @@ -0,0 +1,130 @@ +currentPath = $current_path; + $this->pathMatcher = $path_matcher; + $this->requestStack = $request_stack; + $this->languageManager = $language_manager; + $this->redirectStorage = $redirect_storage; + $this->config = $config->get('redirect_404.settings'); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[KernelEvents::EXCEPTION][] = 'onKernelException'; + return $events; + } + + /** + * Logs an exception of 404 Redirect errors. + * + * @param GetResponseForExceptionEvent $event + * Is given by the event dispatcher. + */ + public function onKernelException(GetResponseForExceptionEvent $event) { + // Only log page not found (404) errors. + if ($event->getException() instanceof NotFoundHttpException) { + $path = $this->currentPath->getPath(); + + // Ignore paths specified in the redirect settings. + if ($pages = Unicode::strtolower($this->config->get('pages'))) { + // Do not trim a trailing slash if that is the complete path. + $path_to_match = $path === '/' ? $path : rtrim($path, '/'); + + if ($this->pathMatcher->matchPath(Unicode::strtolower($path_to_match), $pages)) { + return; + } + } + + // Allow to store paths with arguments. + if ($query_string = $this->requestStack->getCurrentRequest()->getQueryString()) { + $query_string = '?' . $query_string; + } + $path .= $query_string; + $langcode = $this->languageManager->getCurrentLanguage()->getId(); + + // Write record. + $this->redirectStorage->logRequest($path, $langcode); + } + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php new file mode 100644 index 000000000..7e01d1af5 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php @@ -0,0 +1,193 @@ +languageManager = $language_manager; + $this->redirectStorage = $redirect_storage; + $this->dateFormatter = $date_formatter; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('language_manager'), + $container->get('redirect.not_found_storage'), + $container->get('date.formatter'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'redirect_fix_404_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $destination = $this->getDestinationArray(); + + $search = $this->getRequest()->get('search'); + $form['#attributes'] = ['class' => ['search-form']]; + + $form['basic'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Filter 404s'), + '#attributes' => ['class' => ['container-inline']], + ]; + $form['basic']['filter'] = [ + '#type' => 'textfield', + '#title' => '', + '#default_value' => $search, + '#maxlength' => 128, + '#size' => 25, + ]; + $form['basic']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Filter'), + '#action' => 'filter', + ]; + if ($search) { + $form['basic']['reset'] = [ + '#type' => 'submit', + '#value' => $this->t('Reset'), + '#action' => 'reset', + ]; + } + + $languages = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL); + $multilingual = $this->languageManager->isMultilingual(); + + $header = [ + ['data' => $this->t('Path'), 'field' => 'source'], + ['data' => $this->t('Count'), 'field' => 'count', 'sort' => 'desc'], + ['data' => $this->t('Last accessed'), 'field' => 'timestamp'], + ]; + if ($multilingual) { + $header[] = ['data' => $this->t('Language'), 'field' => 'language']; + } + $header[] = ['data' => $this->t('Operations')]; + + $rows = []; + $results = $this->redirectStorage->listRequests($header, $search); + foreach ($results as $result) { + $path = ltrim($result->path, '/'); + + $row = []; + $row['source'] = $path; + $row['count'] = $result->count; + $row['timestamp'] = $this->dateFormatter->format($result->timestamp, 'short'); + if ($multilingual) { + if (isset($languages[$result->langcode])) { + $row['language'] = $languages[$result->langcode]->getName(); + } + else { + $row['language'] = $this->t('Undefined @langcode', ['@langcode' => $result->langcode]); + } + } + + $operations = []; + if ($this->entityTypeManager->getAccessControlHandler('redirect')->createAccess()) { + $operations['add'] = [ + 'title' => $this->t('Add redirect'), + 'url' => Url::fromRoute('redirect.add', [], ['query' => ['source' => $path, 'language' => $result->langcode] + $destination]), + ]; + } + $row['operations'] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $operations, + ], + ]; + + $rows[] = $row; + } + + $form['redirect_404_table'] = [ + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('There are no 404 errors to fix.'), + ]; + $form['redirect_404_pager'] = ['#type' => 'pager']; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + if ($form_state->getTriggeringElement()['#action'] == 'filter') { + $form_state->setRedirect('redirect_404.fix_404', [], ['query' => ['search' => trim($form_state->getValue('filter'))]]); + } + else { + $form_state->setRedirect('redirect_404.fix_404'); + } + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Language.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Language.php new file mode 100644 index 000000000..4fc03d334 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Language.php @@ -0,0 +1,63 @@ +languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account) { + return $this->languageManager->isMultilingual(); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php new file mode 100644 index 000000000..b1c0e6da5 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php @@ -0,0 +1,123 @@ +entityTypeManager = $entity_type_manager; + $this->renderer = $renderer; + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('renderer'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function clickSortable() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function render(ResultRow $values) { + $links = []; + + $query = [ + 'query' => [ + 'source' => ltrim($this->getValue($values, 'path'), '/'), + 'language' => $this->getValue($values, 'langcode'), + 'destination' => $this->view->getPath(), + ], + ]; + $links['add'] = [ + 'title' => $this->t('Add redirect'), + 'url' => Url::fromRoute('redirect.add', [], $query), + ]; + + if ($this->currentUser->hasPermission('administer redirect settings')) { + $links['ignore'] = [ + 'title' => $this->t('Ignore'), + 'url' => Url::fromRoute('redirect_404.ignore_404', [ + 'path' => $this->getValue($values, 'path'), + 'langcode' => $this->getValue($values, 'langcode'), + ]), + ]; + } + + $operations['data'] = [ + '#type' => 'operations', + '#links' => $links, + ]; + + return $this->renderer->render($operations); + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account) { + return $this->entityTypeManager->getAccessControlHandler('redirect')->createAccess(); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/RedirectNotFoundStorageInterface.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/RedirectNotFoundStorageInterface.php new file mode 100644 index 000000000..27bc56de5 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/RedirectNotFoundStorageInterface.php @@ -0,0 +1,53 @@ +loggerChannelFactory = $logger_channel_factory; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function get($channel) { + if ($channel == 'page not found' && $this->configFactory->get('redirect_404.settings')->get('suppress_404')) { + // Do not log if a 404 error is detected and the suppress_404 is enabled. + return new NullLogger(); + } + + // Call LoggerChannelFactory to let the default logger workflow proceed. + return $this->loggerChannelFactory->get($channel); + } + + /** + * {@inheritdoc} + */ + public function addLogger(LoggerInterface $logger, $priority = 0) { + $this->loggerChannelFactory->addLogger($logger, $priority); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/SqlRedirectNotFoundStorage.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/SqlRedirectNotFoundStorage.php new file mode 100644 index 000000000..8b425e9ba --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/SqlRedirectNotFoundStorage.php @@ -0,0 +1,162 @@ +database = $database; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function logRequest($path, $langcode) { + if (Unicode::strlen($path) > static::MAX_PATH_LENGTH) { + // Don't attempt to log paths that would result in an exception. There is + // no point in logging truncated paths, as they cannot be used to build a + // new redirect. + return; + } + // Ignore invalid UTF-8, which can't be logged. + if (!Unicode::validateUtf8($path)) { + return; + } + + // If the request is not new, update its count and timestamp. + $this->database->merge('redirect_404') + ->key('path', $path) + ->key('langcode', $langcode) + ->expression('count', 'count + 1') + ->fields([ + 'timestamp' => REQUEST_TIME, + 'count' => 1, + 'resolved' => 0, + ]) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function resolveLogRequest($path, $langcode) { + $this->database->update('redirect_404') + ->fields(['resolved' => 1]) + ->condition('path', $path) + ->condition('langcode', $langcode) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function purgeOldRequests() { + $row_limit = $this->configFactory->get('redirect_404.settings')->get('row_limit'); + + // In admin form 0 used as value for 'All' label. + if ($row_limit == 0) { + return; + } + + $query = $this->database->select('redirect_404', 'r404'); + $query->fields('r404', ['timestamp']); + // On databases known to support log(), use it to calculate a logarithmic + // scale of the count, to delete records with count of 1-9 first, then + // 10-99 and so on. + if ($this->database->driver() == 'mysql' || $this->database->driver() == 'pgsql') { + $query->addExpression('floor(log(10, count))', 'count_log'); + $query->orderBy('count_log', 'DESC'); + } + $query->orderBy('timestamp', 'DESC'); + $cutoff = $query + ->range($row_limit, 1) + ->execute() + ->fetchAssoc(); + + if (!empty($cutoff)) { + // Delete records having older timestamp and less visits (on a logarithmic + // scale) than cutoff. + $delete_query = $this->database->delete('redirect_404'); + + if ($this->database->driver() == 'mysql' || $this->database->driver() == 'pgsql') { + // Delete rows with same count_log AND older timestamp than cutoff. + $and_condition = $delete_query->andConditionGroup() + ->where('floor(log(10, count)) = :count_log2', [':count_log2' => $cutoff['count_log']]) + ->condition('timestamp', $cutoff['timestamp'], '<='); + + // And delete all the rows with count_log less than the cutoff. + $condition = $delete_query->orConditionGroup() + ->where('floor(log(10, count)) < :count_log1', [':count_log1' => $cutoff['count_log']]) + ->condition($and_condition); + $delete_query->condition($condition); + } + else { + $delete_query->condition('timestamp', $cutoff['timestamp'], '<='); + } + $delete_query->execute(); + } + } + + /** + * {@inheritdoc} + */ + public function listRequests(array $header = [], $search = NULL) { + $query = $this->database + ->select('redirect_404', 'r404') + ->extend('Drupal\Core\Database\Query\TableSortExtender') + ->orderByHeader($header) + ->extend('Drupal\Core\Database\Query\PagerSelectExtender') + ->limit(25) + ->fields('r404'); + + if ($search) { + // Replace wildcards with PDO wildcards. + // @todo Find a way to write a nicer pattern. + $wildcard = '%' . trim(preg_replace('!\*+!', '%', $this->database->escapeLike($search)), '%') . '%'; + $query->condition('path', $wildcard, 'LIKE'); + } + $results = $query->condition('resolved', 0, '=')->execute()->fetchAll(); + + return $results; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php new file mode 100644 index 000000000..c7657447c --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php @@ -0,0 +1,202 @@ +save(); + $language = ConfigurableLanguage::createFromLangcode('es'); + $language->save(); + $language = ConfigurableLanguage::createFromLangcode('fr'); + $language->save(); + } + + /** + * Tests the fix 404 pages workflow with language and content translation. + */ + public function testFix404RedirectList() { + // Visit a non existing page to have the 404 redirect_error entry. + $this->drupalGet('fr/testing'); + + $redirect = \Drupal::database()->select('redirect_404') + ->fields('redirect_404') + ->condition('path', '/testing') + ->execute() + ->fetchAll(); + if (count($redirect) == 0) { + $this->fail('No record was added'); + } + + // Go to the "fix 404" page and check the listing. + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('testing'); + $this->assertLanguageInTableBody('French'); + // Check the Language view filter uses the default language filter. + $this->assertOption('edit-langcode', 'All'); + $this->assertOption('edit-langcode', 'en'); + $this->assertOption('edit-langcode', 'de'); + $this->assertOption('edit-langcode', 'es'); + $this->assertOption('edit-langcode', 'fr'); + $this->assertOption('edit-langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED); + $this->clickLink(t('Add redirect')); + + // Check if we generate correct Add redirect url and if the form is + // pre-filled. + $destination = Url::fromRoute('redirect_404.fix_404')->getInternalPath(); + $options = [ + 'query' => [ + 'source' => 'testing', + 'language' => 'fr', + 'destination' => $destination, + ] + ]; + $this->assertUrl('admin/config/search/redirect/add', $options); + $this->assertFieldByName('redirect_source[0][path]', 'testing'); + $this->assertOptionSelected('edit-language-0-value', 'fr'); + // Save the redirect. + $edit = ['redirect_redirect[0][uri]' => '/node']; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertUrl('admin/config/search/redirect/404'); + $this->assertText('There are no 404 errors to fix.'); + // Check if the redirect works as expected. + $this->assertRedirect('fr/testing', 'fr/node', 'HTTP/1.1 301 Moved Permanently'); + + // Test removing a redirect assignment, visit again the non existing page. + $this->drupalGet('admin/config/search/redirect'); + $this->assertText('testing'); + $this->assertLanguageInTableBody('French'); + $this->clickLink('Delete', 0); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertUrl('admin/config/search/redirect'); + $this->assertText('There is no redirect yet.'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('There are no 404 errors to fix.'); + // Should be listed again in the 404 overview. + $this->drupalGet('fr/testing'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertLanguageInTableBody('French'); + // Check the error path visit count. + $this->assertFieldByXPath('//table/tbody/tr/td[2]', 2); + $this->clickLink('Add redirect'); + // Save the redirect with a different langcode. + $this->assertFieldByName('redirect_source[0][path]', 'testing'); + $this->assertOptionSelected('edit-language-0-value', 'fr'); + $edit['language[0][value]'] = 'es'; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertUrl('admin/config/search/redirect/404'); + // Should still be listed, redirecting to another language does not resolve + // the path. + $this->assertLanguageInTableBody('French'); + $this->drupalGet('admin/config/search/redirect'); + $this->assertLanguageInTableBody('Spanish'); + // Check if the redirect works as expected. + $this->assertRedirect('es/testing', 'es/node', 'HTTP/1.1 301 Moved Permanently'); + + // Visit multiple non existing pages to test the Redirect 404 View. + $this->drupalGet('testing1'); + $this->drupalGet('de/testing2'); + $this->drupalGet('de/testing2?test=1'); + $this->drupalGet('de/testing2?test=2'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertLanguageInTableBody('French'); + $this->assertLanguageInTableBody('English'); + $this->assertLanguageInTableBody('German'); + $this->assertText('testing1'); + $this->assertText('testing2'); + $this->assertText('testing2?test=1'); + $this->assertText('testing2?test=2'); + + // Test the Language view filter. + $this->drupalGet('admin/config/search/redirect/404', ['query' => ['langcode' => 'de']]); + $this->assertText('English'); + $this->assertNoLanguageInTableBody('English'); + $this->assertLanguageInTableBody('German'); + $this->assertNoText('testing1'); + $this->assertText('testing2'); + $this->assertText('testing2?test=1'); + $this->assertText('testing2?test=2'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertLanguageInTableBody('English'); + $this->assertLanguageInTableBody('German'); + $this->assertText('testing1'); + $this->assertText('testing2'); + $this->assertText('testing2?test=1'); + $this->assertText('testing2?test=2'); + $this->drupalGet('admin/config/search/redirect/404', ['query' => ['langcode' => 'en']]); + $this->assertLanguageInTableBody('English'); + $this->assertNoLanguageInTableBody('German'); + $this->assertText('testing1'); + $this->assertNoText('testing2'); + $this->assertNoText('testing2?test=1'); + $this->assertNoText('testing2?test=2'); + + // Assign a redirect to 'testing1'. + $this->clickLink('Add redirect'); + $options = [ + 'query' => [ + 'source' => 'testing1', + 'language' => 'en', + 'destination' => $destination, + ] + ]; + $this->assertUrl('admin/config/search/redirect/add', $options); + $this->assertFieldByName('redirect_source[0][path]', 'testing1'); + $this->assertOptionSelected('edit-language-0-value', 'en'); + $edit = ['redirect_redirect[0][uri]' => '/node']; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertUrl('admin/config/search/redirect/404'); + $this->assertNoLanguageInTableBody('English'); + $this->assertLanguageInTableBody('German'); + $this->drupalGet('admin/config/search/redirect'); + $this->assertLanguageInTableBody('Spanish'); + $this->assertLanguageInTableBody('English'); + // Check if the redirect works as expected. + $this->assertRedirect('/testing1', '/node', 'HTTP/1.1 301 Moved Permanently'); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php new file mode 100644 index 000000000..44b664459 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php @@ -0,0 +1,185 @@ +drupalGet('non-existing0'); + + // Go to the "fix 404" page and check the listing. + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('non-existing0'); + $this->clickLink(t('Add redirect')); + + // Check if we generate correct Add redirect url and if the form is + // pre-filled. + $destination = Url::fromRoute('redirect_404.fix_404')->getInternalPath(); + $options = [ + 'query' => [ + 'source' => 'non-existing0', + 'language' => 'en', + 'destination' => $destination, + ] + ]; + $this->assertUrl('admin/config/search/redirect/add', $options); + $this->assertFieldByName('redirect_source[0][path]', 'non-existing0'); + // Save the redirect. + $edit = ['redirect_redirect[0][uri]' => '/node']; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertUrl('admin/config/search/redirect/404'); + $this->assertText('There are no 404 errors to fix.'); + // Check if the redirect works as expected. + $this->drupalGet('non-existing0'); + $this->assertUrl('node'); + + // Test removing a redirect assignment, visit again the non existing page. + $this->drupalGet('admin/config/search/redirect'); + $this->assertText('non-existing0'); + $this->clickLink('Delete', 0); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertUrl('admin/config/search/redirect'); + $this->assertText('There is no redirect yet.'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('There are no 404 errors to fix.'); + // Should be listed again in the 404 overview. + $this->drupalGet('non-existing0'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('non-existing0'); + + // Visit multiple non existing pages to test the Redirect 404 View. + $this->drupalGet('non-existing0?test=1'); + $this->drupalGet('non-existing0?test=2'); + $this->drupalGet('non-existing1'); + $this->drupalGet('non-existing2'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('non-existing0?test=1'); + $this->assertText('non-existing0?test=2'); + $this->assertText('non-existing0'); + $this->assertText('non-existing1'); + $this->assertText('non-existing2'); + + // Test the Path view filter. + $this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'test=']]); + $this->assertText('non-existing0?test=1'); + $this->assertText('non-existing0?test=2'); + $this->assertNoText('non-existing1'); + $this->assertNoText('non-existing2'); + $this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'existing1']]); + $this->assertNoText('non-existing0?test=1'); + $this->assertNoText('non-existing0?test=2'); + $this->assertNoText('non-existing0'); + $this->assertText('non-existing1'); + $this->assertNoText('non-existing2'); + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('non-existing0?test=1'); + $this->assertText('non-existing0?test=2'); + $this->assertText('non-existing0'); + $this->assertText('non-existing1'); + $this->assertText('non-existing2'); + $this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'g2']]); + $this->assertNoText('non-existing0?test=1'); + $this->assertNoText('non-existing0?test=2'); + $this->assertNoText('non-existing0'); + $this->assertNoText('non-existing1'); + $this->assertText('non-existing2'); + + // Assign a redirect to 'non-existing2'. + $this->clickLink('Add redirect'); + $options = [ + 'query' => [ + 'source' => 'non-existing2', + 'language' => 'en', + 'destination' => $destination, + ] + ]; + $this->assertUrl('admin/config/search/redirect/add', $options); + $this->assertFieldByName('redirect_source[0][path]', 'non-existing2'); + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertUrl('admin/config/search/redirect/404'); + $this->assertText('non-existing0?test=1'); + $this->assertText('non-existing0?test=2'); + $this->assertText('non-existing0'); + $this->assertText('non-existing1'); + $this->assertNoText('non-existing2'); + // Check if the redirect works as expected. + $this->drupalGet('admin/config/search/redirect'); + $this->assertText('non-existing2'); + } + + /** + * Tests the redirect ignore pages. + */ + public function testIgnorePages() { + // Create two nodes. + $node1 = $this->drupalCreateNode(['type' => 'page']); + $node2 = $this->drupalCreateNode(['type' => 'page']); + + // Set some pages to be ignored just for the test. + $node_to_ignore = '/node/' . $node1->id() . '/test'; + $terms_to_ignore = '/term/*'; + $pages = $node_to_ignore . "\r\n" . $terms_to_ignore; + \Drupal::configFactory() + ->getEditable('redirect_404.settings') + ->set('pages', $pages) + ->save(); + + // Visit ignored or non existing pages. + $this->drupalGet('node/' . $node1->id() . '/test'); + $this->drupalGet('term/foo'); + $this->drupalGet('term/1'); + // Go to the "fix 404" page and check there are no 404 entries. + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertNoText('node/' . $node1->id() . '/test'); + $this->assertNoText('term/foo'); + $this->assertNoText('term/1'); + + // Visit non existing but 'unignored' page. + $this->drupalGet('node/' . $node2->id() . '/test'); + // Go to the "fix 404" page and check there is a 404 entry. + $this->drupalGet('admin/config/search/redirect/404'); + $this->assertText('node/' . $node2->id() . '/test'); + + // Add this 404 entry to the 'ignore path' list, assert it works properly. + $path_to_ignore = '/node/' . $node2->id() . '/test'; + $destination = '&destination=admin/config/search/redirect/404'; + $this->clickLink('Ignore'); + $this->assertUrl('admin/config/search/redirect/settings?ignore=' . $path_to_ignore . $destination); + $this->assertText('Resolved the path ' . $path_to_ignore . ' in the database. Please check the ignored list and save the settings.'); + $xpath = $this->xpath('//*[@id="edit-ignore-pages"]')[0]->asXML(); + $this->assertTrue(strpos($xpath, $node_to_ignore), $node_to_ignore . " in 'Path to ignore' found"); + $this->assertTrue(strpos($xpath, $terms_to_ignore), $terms_to_ignore . " in 'Path to ignore' found"); + $this->assertTrue(strpos($xpath, $path_to_ignore), $path_to_ignore . " in 'Path to ignore' found"); + + // Save the path with wildcard, but omitting the leading slash. + $nodes_to_ignore = 'node/*'; + $edit = ['ignore_pages' => $nodes_to_ignore . "\r\n" . $terms_to_ignore]; + $this->drupalPostForm(NULL, $edit, 'Save configuration'); + // Should redirect to 'Fix 404'. Check the 404 entry is not shown anymore. + $this->assertUrl('admin/config/search/redirect/404'); + $this->assertText('Configuration was saved.'); + $this->assertNoText('node/' . $node2->id() . '/test'); + $this->assertText('There are no 404 errors to fix.'); + + // Go back to the settings to check the 'Path to ignore' configurations. + $this->drupalGet('admin/config/search/redirect/settings'); + $xpath = $this->xpath('//*[@id="edit-ignore-pages"]')[0]->asXML(); + // Check that the new page to ignore has been saved with leading slash. + $this->assertTrue(strpos($xpath, '/' . $nodes_to_ignore), '/' . $nodes_to_ignore . " in 'Path to ignore' found"); + $this->assertTrue(strpos($xpath, $terms_to_ignore), $terms_to_ignore . " in 'Path to ignore' found"); + $this->assertFalse(strpos($xpath, $node_to_ignore), $node_to_ignore . " in 'Path to ignore' found"); + $this->assertFalse(strpos($xpath, $path_to_ignore), $path_to_ignore . " in 'Path to ignore' found"); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404LogSuppressorTest.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404LogSuppressorTest.php new file mode 100644 index 000000000..ef021b553 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404LogSuppressorTest.php @@ -0,0 +1,80 @@ +adminUser = $this->drupalCreateUser([ + 'administer redirect settings', + 'administer redirects', + ]); + $this->webUser = $this->drupalCreateUser([]); + } + + /** + * Tests the suppress_404 service. + */ + public function testSuppress404Events() { + // Cause a page not found and an access denied event. + $this->drupalGet('page-not-found'); + $this->assertResponse(404); + $this->drupalLogin($this->webUser); + $this->drupalGet('admin/reports/dblog'); + $this->assertResponse(403); + + // Assert the events are logged in the dblog reports. + $this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'page not found'")->fetchField(), 1); + $this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'access denied'")->fetchField(), 1); + + // Login as admin and enable suppress_404 to avoid logging the 404 event. + $this->drupalLogin($this->adminUser); + $edit = ['suppress_404' => TRUE]; + $this->drupalPostForm('admin/config/search/redirect/settings', $edit, 'Save configuration'); + + // Cause again a page not found and an access denied event. + $this->drupalGet('page-not-found'); + $this->assertResponse(404); + $this->drupalLogin($this->webUser); + $this->drupalGet('admin/reports/dblog'); + $this->assertResponse(403); + + // Assert only the new access denied event is logged now. + $this->drupalLogin($this->adminUser); + $this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'page not found'")->fetchField(), 1); + $this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'access denied'")->fetchField(), 2); + + } +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404TestBase.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404TestBase.php new file mode 100644 index 000000000..2842aeb14 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/src/Tests/Redirect404TestBase.php @@ -0,0 +1,144 @@ +adminUser = $this->drupalCreateUser($this->adminPermissions); + $this->drupalLogin($this->adminUser); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + } + + /** + * Passes if the language of the 404 path IS found on the loaded page. + * + * Because assertText() checks also in the Language select options, this + * specific assertion in the redirect 404 table body is needed. + * + * @param string $language + * The language to assert in the redirect 404 table body. + * @param string $body + * (optional) The table body xpath where to assert the language. Defaults + * to '//table/tbody'. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * + * @return bool + * TRUE on pass, FALSE on fail. + */ + protected function assertLanguageInTableBody($language, $body = '//table/tbody', $message = '') { + return $this->assertLanguageInTableBodyHelper($language, $body, $message, FALSE); + } + + /** + * Passes if the language of the 404 path is NOT found on the loaded page. + * + * Because assertText() checks also in the Language select options, this + * specific assertion in the redirect 404 table body is needed. + * + * @param string $language + * The language to assert in the redirect 404 table body. + * @param string $body + * (optional) The table body xpath where to assert the language. Defaults + * to '//table/tbody'. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * + * @return bool + * TRUE on pass, FALSE on fail. + */ + protected function assertNoLanguageInTableBody($language, $body = '//table/tbody', $message = '') { + return $this->assertLanguageInTableBodyHelper($language, $body, $message, TRUE); + } + + /** + * Helper for assertLanguageInTableBody and assertNoLanguageInTableBody. + * + * @param array $language + * The language to assert in the redirect 404 table body. + * @param string $body + * (optional) The table body xpath where to assert the language. Defaults + * to '//table/tbody'. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * @param bool $not_exists + * (optional) TRUE if this language should not exist, FALSE if it should. + * Defaults to TRUE. + * + * @return bool + * TRUE on pass, FALSE on fail. + */ + protected function assertLanguageInTableBodyHelper($language, $body = '//table/tbody', $message = '', $not_exists = TRUE) { + if (!$message) { + if (!$not_exists) { + $message = new FormattableMarkup('Language "@language" found in 404 table.', ['@language' => $language]); + } + else { + $message = new FormattableMarkup('Language "@language" not found in 404 table.', ['@language' => $language]); + } + } + + if ($not_exists) { + return $this->assertFalse(strpos($this->xpath($body)[0]->asXML(), $language), $message); + } + else { + return $this->assertTrue(strpos($this->xpath($body)[0]->asXML(), $language), $message); + } + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php new file mode 100644 index 000000000..6ed6d6774 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php @@ -0,0 +1,192 @@ +installSchema('redirect_404', 'redirect_404'); + + // Insert some records in the test table with a given count and timestamp. + $this->insert404Row('/test1', 12, strtotime('now')); + $this->insert404Row('/test2', 5, strtotime('-1 hour')); + $this->insert404Row('/test3', 315, strtotime('-1 week')); + $this->insert404Row('/test4', 300, strtotime('-1 month')); + $this->insert404Row('/test5', 1557, strtotime('-1 week')); + $this->insert404Row('/test6', 1, strtotime('-1 day')); + } + + /** + * Tests adding and deleting rows from redirect_404 table. + */ + function testRedirect404CronJob() { + // Set the limit to 3 just for the test. + \Drupal::configFactory() + ->getEditable('redirect_404.settings') + ->set('row_limit', 3) + ->save(); + + // Check that there are 6 rows in the redirect_404 table. + $result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField(); + $this->assertEquals(6, $result); + + // Run cron to drop 3 rows from the redirect_404 test table. + redirect_404_cron(); + + $result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField(); + $this->assertEquals(3, $result); + + // Check there are only 3 rows with more count in the redirect_404 table. + if (\Drupal::database()->driver() == 'mysql' || \Drupal::database()->driver() == 'pgsql') { + $this->assertNo404Row('/test1'); + $this->assertNo404Row('/test2'); + $this->assert404Row('/test3'); + $this->assert404Row('/test4'); + $this->assert404Row('/test5'); + $this->assertNo404Row('/test6'); + } + else { + // In SQLite is the opposite: the 3 rows kept are the newest ones. + $this->assert404Row('/test1'); + $this->assert404Row('/test2'); + $this->assertNo404Row('/test3'); + $this->assertNo404Row('/test4'); + $this->assertNo404Row('/test5'); + $this->assert404Row('/test6'); + } + } + + /** + * Tests adding rows and deleting one row from redirect_404 table. + */ + function testRedirect404CronJobKeepAllButOne() { + // Set the limit to 5 just for the test. + \Drupal::configFactory() + ->getEditable('redirect_404.settings') + ->set('row_limit', 5) + ->save(); + + // Check that there are 6 rows in the redirect_404 table. + $result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField(); + $this->assertEquals(6, $result); + + // Run cron to drop just 1 row from the redirect_404 test table. + redirect_404_cron(); + + $result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField(); + $this->assertEquals(5, $result); + + // Check only the row with least count has been removed from the table. + if (\Drupal::database()->driver() == 'mysql' || \Drupal::database()->driver() == 'pgsql') { + $this->assert404Row('/test1'); + $this->assert404Row('/test2'); + $this->assert404Row('/test3'); + $this->assert404Row('/test4'); + $this->assert404Row('/test5'); + $this->assertNo404Row('/test6'); + } + else { + // In SQlite, only the oldest row is deleted. + $this->assert404Row('/test1'); + $this->assert404Row('/test2'); + $this->assert404Row('/test3'); + $this->assertNo404Row('/test4'); + $this->assert404Row('/test5'); + $this->assert404Row('/test6'); + } + } + + /** + * Inserts a 404 request log in the redirect_404 test table. + * + * @param string $path + * The path of the request. + * @param int $count + * (optional) The visits count of the request. + * @param int $timestamp + * (optional) The timestamp of the last visited request. + * @param string $langcode + * (optional) The langcode of the request. + */ + protected function insert404Row($path, $count = 1, $timestamp = 0, $langcode = 'en') { + db_insert('redirect_404') + ->fields([ + 'path' => $path, + 'langcode' => $langcode, + 'count' => $count, + 'timestamp' => $timestamp, + 'resolved' => 0, + ]) + ->execute(); + } + + /** + * Passes if the row with the given parameters is in the redirect_404 table. + * + * @param string $path + * The path of the request. + * @param string $langcode + * (optional) The langcode of the request. + */ + protected function assert404Row($path, $langcode = 'en') { + $this->assert404RowHelper($path, $langcode, FALSE); + } + + /** + * Passes if the row with the given parameters is NOT in the redirect_404 table. + * + * @param string $path + * The path of the request. + * @param string $langcode + * (optional) The langcode of the request. + */ + protected function assertNo404Row($path, $langcode = 'en') { + $this->assert404RowHelper($path, $langcode, TRUE); + } + + /** + * Passes if the row with the given parameters is in the redirect_404 table. + * + * @param string $path + * The path of the request. + * @param string $langcode + * (optional) The langcode of the request. + * @param bool $not_exists + * (optional) TRUE if this 404 row should not exist in the redirect_404 + * table, FALSE if it should. Defaults to TRUE. + */ + protected function assert404RowHelper($path, $langcode = 'en', $not_exists = TRUE) { + $result = db_select('redirect_404', 'r404') + ->fields('r404', ['path']) + ->condition('path', $path) + ->condition('langcode', $langcode) + ->condition('resolved', 0) + ->execute() + ->fetchField(); + + if ($not_exists) { + $this->assertNotEquals($path, $result); + } + else { + $this->assertEquals($path, $result); + } + } +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php new file mode 100644 index 000000000..42e0224e5 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php @@ -0,0 +1,78 @@ +database = $this->getMockBuilder(Connection::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * Tests that long paths aren't stored in the database. + */ + public function testLongPath() { + $this->database->expects($this->never()) + ->method('merge'); + $storage = new SqlRedirectNotFoundStorage($this->database, $this->getConfigFactoryStub()); + $storage->logRequest($this->randomMachineName(SqlRedirectNotFoundStorage::MAX_PATH_LENGTH + 1), LanguageInterface::LANGCODE_DEFAULT); + } + + /** + * Tests that invalid UTF-8 paths are not stored in the database. + */ + public function testInvalidUtf8Path() { + $this->database->expects($this->never()) + ->method('merge'); + $storage = new SqlRedirectNotFoundStorage($this->database, $this->getConfigFactoryStub()); + $storage->logRequest("Caf\xc3", LanguageInterface::LANGCODE_DEFAULT); + } + + /** + * Tests that all logs are kept if row limit config is "All". + */ + public function testPurgeOldRequests() { + $this->configFactory = $this->getConfigFactoryStub( + [ + 'redirect_404.settings' => [ + 'row_limit' => 0, + ], + ] + ); + $storage = new SqlRedirectNotFoundStorage($this->database, $this->configFactory); + $storage->purgeOldRequests(); + $this->database->expects($this->never()) + ->method('select'); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/install/redirect_domain.domains.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/install/redirect_domain.domains.yml new file mode 100644 index 000000000..05c0310cc --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/install/redirect_domain.domains.yml @@ -0,0 +1 @@ +domain_redirects: { } diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/schema/redirect_domain.schema.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/schema/redirect_domain.schema.yml new file mode 100644 index 000000000..5e29a4c57 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/config/schema/redirect_domain.schema.yml @@ -0,0 +1,18 @@ +redirect_domain.domains: + type: config_object + label: 'Redirect domains' + mapping: + domain_redirects: + type: sequence + label: 'Domain redirects' + sequence: + type: sequence + sequence: + type: mapping + mapping: + sub_path: + type: string + label: 'Sub path' + destination: + type: string + label: 'Destination' diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.info.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.info.yml new file mode 100644 index 000000000..31153f01c --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.info.yml @@ -0,0 +1,13 @@ +name: 'Redirect Domain' +type: module +description: 'Allows users to redirect between domains.' +# core: 8.x + +dependencies: + - redirect + +# Information added by Drupal.org packaging script on 2018-03-12 +version: '8.x-1.1' +core: '8.x' +project: 'redirect' +datestamp: 1520897884 diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.links.task.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.links.task.yml new file mode 100644 index 000000000..1dcee94c0 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.links.task.yml @@ -0,0 +1,5 @@ +redirect_domain.domain_list: + title: 'Domain redirects' + base_route: redirect.list + route_name: redirect_domain.domain_list + weight: 60 diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.module b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.module new file mode 100644 index 000000000..f60b329bf --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.module @@ -0,0 +1,33 @@ +' . t('About') . ''; + $output .= '

' . t('The Redirect domain module allows users to redirect between domains.') . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
' . t('Manage domain redirects') . '
'; + $output .= '
' . t('The domain redirect is accessed through Domain Redirects. The user can add the domain redirects through the domain redirect table which consists of the domain from which it needs to be redirected, the sub path and the complete url destination to which it needs to be redirected. The module also supports the usage of a wildcard redirecting, thus many requests can be handled with one instance of domain redirect.', [':domainlist' => Url::fromRoute('redirect_domain.domain_list')->toString()]) . '
'; + return $output; + case 'redirect_domain.domain_list': + $output = '

' . t('The domain redirect table consists of the domain from which it needs to be redirected, the sub path and the complete url destination to which it needs to be redirected.') . '

'; + $output .= '
' . t('Example Configuration') . '
'; + $output .= '
    '; + $output .= '
  • example.com/redirect => redirected.com/example-path
  • '; + $output .= '
  • foo.com/* => bar.com
  • '; + $output .= '
'; + $output .= '
' . t('Example Redirects') . '
'; + $output .= '
    '; + $output .= '
  • ' . t('Request: example.com/redirect => Response: redirected.com/example-path') . '
  • '; + $output .= '
  • ' . t('Request: foo.com/any-path => Response: bar.com') . '
  • '; + $output .= '
'; + return $output; + } +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.routing.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.routing.yml new file mode 100644 index 000000000..ad38a0716 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.routing.yml @@ -0,0 +1,7 @@ +redirect_domain.domain_list: + path: '/admin/config/search/redirect/domain' + defaults: + _title: 'Domain redirects' + _form: '\Drupal\redirect_domain\Form\RedirectDomainForm' + requirements: + _permission: 'administer redirects' diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.services.yml b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.services.yml new file mode 100644 index 000000000..94503c617 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/redirect_domain.services.yml @@ -0,0 +1,6 @@ +services: + redirect_domain.request_subscriber: + class: Drupal\redirect_domain\EventSubscriber\DomainRedirectRequestSubscriber + arguments: ['@config.factory', '@redirect.checker', '@path.matcher'] + tags: + - { name: event_subscriber } diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/EventSubscriber/DomainRedirectRequestSubscriber.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/EventSubscriber/DomainRedirectRequestSubscriber.php new file mode 100644 index 000000000..bc949b81f --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/EventSubscriber/DomainRedirectRequestSubscriber.php @@ -0,0 +1,133 @@ +domainConfig = $config_factory->get('redirect_domain.domains'); + $this->redirectConfig = $config_factory->get('redirect.settings'); + $this->redirectChecker = $redirect_checker; + $this->pathMatcher = $path_matcher; + } + + /** + * Handles the domain redirect if any found. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The event to process. + */ + public function onKernelRequestCheckDomainRedirect(GetResponseEvent $event) { + $request = clone $event->getRequest(); + + if (!$this->redirectChecker->canRedirect($request)) { + return; + } + + // Redirect between domains configuration. + $domains = $this->domainConfig->get('domain_redirects'); + if (!empty($domains)) { + $host = $request->getHost(); + $path = $request->getPathInfo(); + $protocol = $request->getScheme() . '://'; + $destination = NULL; + + // Checks if there is a redirect domain in the configuration. + if (isset($domains[str_replace('.', ':', $host)])) { + foreach ($domains[str_replace('.', ':', $host)] as $item) { + if ($this->pathMatcher->matchPath($path, $item['sub_path'])) { + $destination = $item['destination']; + break; + } + } + if ($destination) { + // Use the default status code from Redirect. + $response = new TrustedRedirectResponse( + $protocol . $destination, + $this->redirectConfig->get('default_status_code') + ); + $event->setResponse($response); + return; + } + } + } + } + + /** + * Prior to set the response it check if we can redirect. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The event object. + * @param \Drupal\Core\Url $url + * The Url where we want to redirect. + */ + protected function setResponse(GetResponseEvent $event, Url $url) { + $request = $event->getRequest(); + + parse_str($request->getQueryString(), $query); + $url->setOption('query', $query); + $url->setAbsolute(TRUE); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // This needs to run before RouterListener::onKernelRequest(), which has + // a priority of 32 and + // RedirectRequestSubscriber::onKernelRequestCheckRedirect(), which has + // a priority of 33. Otherwise, that aborts the request if no matching + // route is found. + $events[KernelEvents::REQUEST][] = ['onKernelRequestCheckDomainRedirect', 34]; + return $events; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Form/RedirectDomainForm.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Form/RedirectDomainForm.php new file mode 100644 index 000000000..ec7b20f69 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Form/RedirectDomainForm.php @@ -0,0 +1,165 @@ +has('maximum_domains')) { + $form_state->set('maximum_domains', 1); + } + + $form['redirects'] = [ + '#type' => 'table', + '#tree' => TRUE, + '#header' => [ + $this->t('From domain'), + $this->t('Sub path'), + $this->t('Destination') + ], + '#prefix' => '
', + '#suffix' => '
', + ]; + + $rows = []; + // Obtain domain redirects from configuration. + if ($domain_redirects = $this->config('redirect_domain.domains')->get('domain_redirects')) { + foreach ($domain_redirects as $key => $value) { + foreach ($value as $item) { + $form['redirects'][] = [ + 'from' => [ + '#type' => 'textfield', + '#value' => str_replace(':','.',$key), + ], + 'sub_path' => [ + '#type' => 'textfield', + '#value' => $item['sub_path'], + ], + 'destination' => [ + '#type' => 'textfield', + '#value' => $item['destination'], + ], + ]; + } + } + } + + // Fields for the new domain redirects. + for ($i = 0; $i < $form_state->get('maximum_domains'); $i++) { + $form['redirects'][] = [ + 'from' => [ + '#type' => 'textfield', + ], + 'sub_path' => [ + '#type' => 'textfield', + '#value' => '/', + ], + 'destination' => [ + '#type' => 'textfield', + ], + ]; + } + + $form['add'] = [ + '#type' => 'submit', + '#value' => $this->t('Add another'), + '#submit' => ['::addAnotherSubmit'], + '#ajax' => [ + 'callback' => '::ajaxAddAnother', + 'wrapper' => 'redirect-domain-wrapper', + ], + ]; + $form['submit'] = [ + '#type' => 'submit', + '#button_type' => 'primary', + '#value' => $this->t('Save'), + ]; + return $form; + } + + /** + * Ajax callback for adding another domain redirect. + * + * @param array $form + * The form structure. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The new domain redirect form part. + */ + public function ajaxAddAnother(array $form, FormStateInterface $form_state) { + return $form['redirects']; + } + + /** + * Submit callback for adding a new domain field. + */ + public function addAnotherSubmit(array $form, FormStateInterface $form_state) { + $form_state->set('maximum_domains', $form_state->get('maximum_domains') + 1); + $form_state->setRebuild(TRUE); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + if ($redirects = $form_state->getValue('redirects')) { + foreach ($redirects as $redirect) { + if (strpos($redirect['from'], '://') !== FALSE) { + $form_state->setErrorByName('redirects', t('No protocol should be included in the redirect domain.')); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $domain_redirects = []; + $domain_config = $this->config('redirect_domain.domains'); + + if ($redirects = $form_state->getValue('redirects')) { + foreach ($redirects as $redirect) { + if (!empty($redirect['from']) && !empty($redirect['destination'])) { + // Replace '.' with ':' for an eligible key. + $redirect['from'] = str_replace('.', ':', $redirect['from']); + $domain_redirects[$redirect['from']][] = [ + 'sub_path' => '/' . ltrim($redirect['sub_path'], '/'), + 'destination' => $redirect['destination'] + ]; + } + } + } + $domain_config->set('domain_redirects', $domain_redirects); + $domain_config->save(); + drupal_set_message(t('The domain redirects have been saved.')); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Tests/RedirectDomainUITest.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Tests/RedirectDomainUITest.php new file mode 100644 index 000000000..d50791f37 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/src/Tests/RedirectDomainUITest.php @@ -0,0 +1,64 @@ +drupalCreateUser([ + 'administer site configuration', + 'access administration pages', + 'administer redirects' + ]); + $this->drupalLogin($user); + $this->drupalGet('/admin/config/search/redirect/domain'); + + // Assert that there are 2 domain redirect fields. + $this->assertFieldByName('redirects[0][from]'); + $this->assertFieldByName('redirects[0][sub_path]'); + $this->assertFieldByName('redirects[0][destination]'); + + // Add another field for new domain redirect. + $this->drupalPostAjaxForm(NULL, [], ['op' => t('Add another')]); + + // Add two new domain redirects. + $edit = [ + 'redirects[0][from]' => 'foo.example.org', + 'redirects[0][sub_path]' => '//sub-path', + 'redirects[0][destination]' => 'www.example.org/foo', + 'redirects[1][from]' => 'bar.example.org', + 'redirects[1][sub_path]' => '', + 'redirects[1][destination]' => 'www.example.org/bar', + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + + // Check the new domain redirects. + $this->assertFieldByName('redirects[0][from]', 'foo.example.org'); + $this->assertFieldByName('redirects[0][destination]', 'www.example.org/foo'); + $this->assertFieldByName('redirects[1][from]', 'bar.example.org'); + $this->assertFieldByName('redirects[1][destination]', 'www.example.org/bar'); + + // Ensure that the sub paths are correct. + $this->assertFieldByName('redirects[0][sub_path]', '/sub-path'); + $this->assertFieldByName('redirects[1][sub_path]', '/'); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/tests/src/Unit/DomainRedirectRequestSubscriberTest.php b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/tests/src/Unit/DomainRedirectRequestSubscriberTest.php new file mode 100644 index 000000000..89453ab9d --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/modules/redirect_domain/tests/src/Unit/DomainRedirectRequestSubscriberTest.php @@ -0,0 +1,144 @@ + [ + 'domain_redirects' => [ + 'foo:com' => [ + [ + 'sub_path' => '/fixedredirect', + 'destination' => 'bar.com/fixedredirect', + ], + [ + 'sub_path' => '/*', + 'destination' => 'bar.com/example', + ], + ], + 'example:com' => [ + [ + 'sub_path' => '/foo/*/bar', + 'destination' => 'example.com/bar/foo', + ], + ], + 'simpleexample:com' => [ + [ + 'sub_path' => '/redirect', + 'destination' => 'redirected.com/redirect', + ], + ], + ], + ], + 'redirect.settings' => [ + 'default_status_code' => 301, + ], + 'system.site' => [ + 'page.front' => '/', + ], + ]; + + // Create a mock redirect checker. + $checker = $this->getMockBuilder(RedirectChecker::class) + ->disableOriginalConstructor() + ->getMock(); + $checker->expects($this->any()) + ->method('canRedirect') + ->will($this->returnValue(TRUE)); + + // Set up the configuration for the requested domain. + $config_factory = $this->getConfigFactoryStub($data); + + // Create a mock path matcher. + $route_match = $this->getMock(RouteMatchInterface::class); + $path_matcher = new PathMatcher($config_factory, $route_match); + + $subscriber = new DomainRedirectRequestSubscriber( + $config_factory, + $checker, + $path_matcher + ); + + // Make a request to the urls from the data provider and get the response. + $event = $this->getGetResponseEventStub($request_url, http_build_query([])); + + // Run the main redirect method. + $subscriber->onKernelRequestCheckDomainRedirect($event); + + // Assert the expected response from the data provider. + if ($response_url) { + $this->assertTrue($event->getResponse() instanceof RedirectResponse); + $response = $event->getResponse(); + // Make sure that the response is properly redirected. + $this->assertEquals($response_url, $response->getTargetUrl()); + $this->assertEquals( + $config_factory->get('redirect.settings')->get('default_status_code'), + $response->getStatusCode() + ); + } + else { + $this->assertNull($event->getResponse()); + } + } + + /** + * Gets response event object. + * + * @param $path_info + * The path info. + * @param $query_string + * The query string in the url. + * + * @return GetResponseEvent + * The response for the request. + */ + protected function getGetResponseEventStub($path_info, $query_string) { + $request = Request::create($path_info . '?' . $query_string, 'GET', [], [], [], ['SCRIPT_NAME' => 'index.php']); + + $http_kernel = $this->getMockBuilder(HttpKernelInterface::class) + ->getMock(); + return new GetResponseEvent($http_kernel, $request, 'test'); + } + + /** + * Data provider for the domain redirects. + * + * @return array + * An array of requests and expected responses for the redirect domains. + */ + public function providerDomains() { + $datasets = []; + $datasets[] = ['http://foo.com/example', 'http://bar.com/example']; + $datasets[] = ['http://example.com/foo/test/bar', 'http://example.com/bar/foo']; + $datasets[] = ['http://simpleexample.com/redirect', 'http://redirected.com/redirect']; + $datasets[] = ['http://nonexisting.com', NULL]; + $datasets[] = ['http://simpleexample.com/wrongpath', NULL]; + $datasets[] = ['http://foo.com/fixedredirect', 'http://bar.com/fixedredirect']; + return $datasets; + } +} diff --git a/sites/all/modules/contrib/admin/redirect/redirect.api.php b/sites/all/modules/contrib/admin/redirect/redirect.api.php new file mode 100644 index 000000000..9ec1bdff7 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.api.php @@ -0,0 +1,142 @@ + $redirect) { + if ($redirect->source !== $source) { + // If the redirects to do not exactly match $source (e.g. case + // insensitive matches), then remove them from the results. + unset($redirects[$rid]); + } + } +} + +/** + * Act on a redirect object about to be shown on the add/edit form. + * + * This hook is invoked from redirect_create(). + * + * @param $redirect + * The redirect that is about to be shown on the add/edit form. + * + * @ingroup redirect_api_hooks + */ +function hook_redirect_prepare($redirect) { + +} + +/** + * Act on a redirect being redirected. + * + * This hook is invoked from redirect_redirect() before the redirect callback + * is invoked. + * + * @param $redirect + * The redirect that is being used for the redirect. + * + * @see redirect_redirect() + * @see drupal_page_is_cacheable() + * @ingroup redirect_api_hooks + */ +function hook_redirect_alter($redirect) { +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/sites/all/modules/contrib/admin/redirect/redirect.drush.inc b/sites/all/modules/contrib/admin/redirect/redirect.drush.inc new file mode 100644 index 000000000..ffe98ce05 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.drush.inc @@ -0,0 +1,72 @@ + 'Create redirects.', + 'drupal dependencies' => array('devel_generate'), + 'arguments' => array( + 'count' => 'Number of redirects to generate.', + ), + 'options' => array( + 'delete' => 'Delete all redirects before generating new ones.', + ), + ); + + return $items; +} + +/** + * Command callback. Generate a number of redirects. + */ +function drush_redirect_generate_redirects($count = NULL) { + if (drush_generate_is_number($count) == FALSE) { + return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', t('Invalid number of redirects.')); + } + module_load_include('inc', 'redirect', 'redirect.generate'); + drush_generate_include_devel(); + redirect_run_unprogressive_batch('redirect_generate_redirects_batch_info', $count, drush_get_option('delete')); +} + +/** + * Perform an unprogressive batch process for CLI. + */ +function redirect_run_unprogressive_batch() { + $batch = batch_get(); + if (!empty($batch)) { + // If there is already something in the batch, don't run. + return FALSE; + } + + $args = func_get_args(); + $batch_callback = array_shift($args); + + if (!lock_acquire($batch_callback)) { + return FALSE; + } + + // Attempt to increase the execution time. + drupal_set_time_limit(240); + + // Build the batch array. + $batch = call_user_func_array($batch_callback, $args); + batch_set($batch); + + // We need to manually set the progressive variable again. + // @todo Remove when http://drupal.org/node/638712 is fixed. + $batch =& batch_get(); + $batch['progressive'] = FALSE; + + // Run the batch process. + batch_process(); + + lock_release($batch_callback); + return TRUE; +} diff --git a/sites/all/modules/contrib/admin/redirect/redirect.generate.inc b/sites/all/modules/contrib/admin/redirect/redirect.generate.inc new file mode 100644 index 000000000..6120ae19b --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.generate.inc @@ -0,0 +1,194 @@ + 'textfield', + '#title' => t('How many URL redirects would you like to generate?'), + '#default_value' => 50, + '#size' => 4, + ); + $form['delete'] = array( + '#type' => 'checkbox', + '#title' => t('Delete all URL redirects before generating new URL redirects.'), + '#default_value' => FALSE, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Generate'), + ); + + return $form; +} + +function redirect_generate_form_submit(&$form, &$form_state) { + // Run the batch. + $batch = redirect_generate_redirects_batch_info($form_state['values']['count'], $form_state['values']['delete']); + batch_set($batch); +} + +function redirect_generate_redirects_batch_info($count, $delete = FALSE) { + if ($delete) { + $operations[] = array('redirect_generate_batch_delete', array()); + } + + $operations[] = array('redirect_generate_batch_generate', array($count)); + + return array( + 'operations' => $operations, + 'finished' => 'redirect_generate_batch_finished', + 'file' => drupal_get_path('module', 'redirect') . '/redirect.generate.inc', + ); +} + +function redirect_generate_batch_delete(array &$context) { + if (empty($context['sandbox'])) { + $context['sandbox'] = array(); + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_rid'] = 0; + $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT rid) FROM {redirect}')->fetchField(); + } + + $limit = 20; + $rids = db_query_range("SELECT rid FROM {redirect} WHERE rid > :rid ORDER BY rid", 0, $limit, array(':rid' => $context['sandbox']['current_rid']))->fetchCol(); + foreach (redirect_repository()->loadMultiple($rids) as $redirect) { + $redirect->delete(); + } + + // Update our progress information. + $context['sandbox']['progress'] += count($rids); + $context['sandbox']['current_rid'] = end($rids); + $context['message'] = t('Deleted URL redirect @rid.', array('@rid' => end($rids))); + + // Inform the batch engine that we are not finished, + // and provide an estimation of the completion level we reached. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']); + } +} + +function redirect_generate_batch_generate($num, array &$context) { + if (empty($context['sandbox'])) { + $context['sandbox'] = array(); + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = $num; + $query = \Drupal::database()->select('node', 'n'); + $query->addField('n', 'nid'); + $query->condition('n.status', NODE_PUBLISHED); + $query->addTag('node_access'); + $context['sandbox']['nids'] = $query->execute()->fetchAllKeyed(0, 0); + } + + module_load_include('inc', 'devel_generate'); + + $limit = 20; + $types = array_keys(redirect_status_code_options()); + $languages = \Drupal::moduleHandler()->moduleExists('locale') ? array_keys(\Drupal::languageManager()->getLanguages()) : array(); + + for ($i = 0; $i < min($limit, $context['sandbox']['max'] - $context['sandbox']['progress']); $i++) { + $rand = mt_rand(0, 100); + + $redirect = Redirect::create(); + + $source = _redirect_generate_url(); + $source_options = array(); + $redirect_options = array(); + + if ($context['sandbox']['nids'] && $rand >= 40) { + $redirect_target = 'node/' . array_rand($context['sandbox']['nids']); + } + else { + $redirect_target = _redirect_generate_url(TRUE); + if ($rand <= 20) { + $redirect_options['query'] = _redirect_generate_querystring(); + } + if ($rand <= 5) { + $redirect_options['fragment'] = DevelGenerateBase::generateWord(mt_rand(4, 8)); + } + } + + if ($rand <= 20) { + $redirect->setStatusCode($types[array_rand($types)]); + } + + if ($languages && $rand <= 20) { + $redirect->setLanguage($languages[array_rand($languages)]); + } + + $query = array(); + if ($rand <= 30) { + $query = _redirect_generate_querystring(); + } + + $redirect->setSource($source, $query); + $redirect->setRedirect($redirect_target); + + $redirect->save(); + + if (mt_rand(0, 1)) { + $query = \Drupal::database(); + $query->update('redirect') + ->fields(array( + 'count' => mt_rand(1, 500), + 'access' => mt_rand(REQUEST_TIME - 31536000, REQUEST_TIME), + )) + ->condition('rid', $redirect->id()) + ->execute(); + } + + $context['results'][] = $redirect->id(); + } + + // Update our progress information. + $context['sandbox']['progress'] += $limit; + //$context['message'] = t('Deleted URL redirect @rid.', array('@rid' => end($rids))); + + // Inform the batch engine that we are not finished, + // and provide an estimation of the completion level we reached. + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']); + } +} + +function redirect_generate_batch_finished($success, $results, $operations) { + if ($success) { + drupal_set_message(\Drupal::translation()->formatPlural(count($results), 'One URL redirect created.', '@count URL redirects created.')); + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE)))); + } +} + +function _redirect_generate_url($external = FALSE, $max_levels = 2) { + $url = array(); + if ($external) { + $tlds = array('com', 'net', 'org'); + $url[] = 'http://www.example.'. $tlds[array_rand($tlds)]; + } + $max_levels = mt_rand($external ? 0 : 1, $max_levels); + for ($i = 1; $i <= $max_levels; $i++) { + $url[] = DevelGenerateBase::generateWord(mt_rand(6 / $i, 8)); + } + return implode('/', $url); +} + +function _redirect_generate_querystring() { + $query = array(DevelGenerateBase::generateWord(mt_rand(1, 3)) => DevelGenerateBase::generateWord(mt_rand(2, 4))); + return $query; +} diff --git a/sites/all/modules/contrib/admin/redirect/redirect.info.yml b/sites/all/modules/contrib/admin/redirect/redirect.info.yml new file mode 100644 index 000000000..b5f4921cd --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.info.yml @@ -0,0 +1,15 @@ +name: Redirect +type: module +description: Allows users to redirect from old URLs to new URLs. +# core: 8.x +configure: redirect.settings + +dependencies: + - drupal:link + - drupal:views + +# Information added by Drupal.org packaging script on 2018-03-12 +version: '8.x-1.1' +core: '8.x' +project: 'redirect' +datestamp: 1520897884 diff --git a/sites/all/modules/contrib/admin/redirect/redirect.install b/sites/all/modules/contrib/admin/redirect/redirect.install new file mode 100644 index 000000000..31a8ee693 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.install @@ -0,0 +1,224 @@ +fetchField(); + } + + $result = \Drupal::database()->select('redirect', 'r') + ->fields('r', ['rid', 'redirect_source__path', 'redirect_source__query', 'language', 'hash']) + ->condition('rid', $sandbox['current_rid'], '>') + ->range(0, 100) + ->orderBy('rid', 'ASC') + ->execute(); + + foreach ($result as $row) { + $query = !empty($row->redirect_source__query) ? unserialize($row->redirect_source__query): []; + $new_hash = Redirect::generateHash($row->redirect_source__path, (array) $query, $row->language); + if ($row->hash != $new_hash) { + // Do a direct query to speed things up. + $query = \Drupal::database(); + $query->update('redirect') + ->fields(['hash' => $new_hash]) + ->condition('rid', $row->rid) + ->execute(); + } + $sandbox['progress']++; + $sandbox['current_rid'] = $row->rid; + } + // Reset caches. + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); +} + + +/** + * Update the {redirect} table. + */ +function redirect_update_8101() { + // Get the current schema, change the length. + $key_value_store_schema = \Drupal::keyValue('entity.storage_schema.sql'); + $schema = $key_value_store_schema->get('redirect.field_schema_data.hash'); + $schema['redirect']['fields']['hash']['length'] = 64; + + // Set the max_length of the hash column to 64 characters. + Database::getConnection() + ->schema() + ->changeField('redirect', 'hash', 'hash', $schema['redirect']['fields']['hash']); + + // Update the last installed field definition and field schema. + /** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value_store */ + \Drupal::entityManager()->clearCachedFieldDefinitions(); + $key_value_store_definition = \Drupal::keyValue('entity.definitions.installed'); + $storage_definitions = $key_value_store_definition->get('redirect.field_storage_definitions'); + $storage_definitions['hash'] = $storage_definition = \Drupal::entityManager() + ->getFieldStorageDefinitions('redirect')['hash']; + $key_value_store_definition->set('redirect.field_storage_definitions', $storage_definitions); + + // Update the stored schema. + $key_value_store_schema->set('redirect.field_schema_data.hash', $schema); +} + +/** + * Update settings based on existing settings and Globalredirect settings. + */ +function redirect_update_8102() { + // Load default configuration. + $redirect_settings = \Drupal::config('redirect.settings'); + $globalredirect_settings = \Drupal::config('globalredirect.settings'); + $config_factory = \Drupal::configFactory(); + $redirect = $config_factory->getEditable('redirect.settings'); + $nonclean_to_clean = $redirect_settings->get('global_clean'); + $admin_path = $redirect_settings->get('global_admin_paths'); + $frontpage_redirect = $redirect_settings->get('global_home'); + $deslash = $redirect_settings->get('global_deslash'); + + $message = NULL; + + // If Globalredirect configuration exists, use those settings. + if ((!$globalredirect_settings->isNew())) { + $access_check = $globalredirect_settings->get('access_check'); + $normalize_aliases = $globalredirect_settings->get('normalize_aliases'); + $content_location_header = $globalredirect_settings->get('content_location_header'); + $term_path_handler = $globalredirect_settings->get('term_path_handler'); + $deslash = $globalredirect_settings->get('deslash'); + $frontpage_redirect = $globalredirect_settings->get('frontpage_redirect'); + $nonclean_to_clean = $globalredirect_settings->get('nonclean_to_clean'); + + $redirect->set('access_check', $access_check); + $redirect->set('normalize_aliases', $normalize_aliases); + $redirect->set('content_location_header', $content_location_header); + $redirect->set('term_path_handler', $term_path_handler); + + $message = 'The Globalredirect module functionality has been merged into redirect, it should be uninstalled now.'; + } + else { + $redirect->set('access_check', FALSE); + $redirect->set('normalize_aliases', TRUE); + $redirect->set('content_location_header', FALSE); + $redirect->set('term_path_handler', TRUE); + } + + // Update new redirect settings names. + $redirect->set('nonclean_to_clean', $nonclean_to_clean); + $redirect->set('admin_path', $admin_path); + $redirect->set('frontpage_redirect', $frontpage_redirect); + $redirect->set('deslash', $deslash); + + // Remove old names of redirect settings. + $redirect->clear('global_clean'); + $redirect->clear('global_admin_paths'); + $redirect->clear('global_home'); + $redirect->clear('global_deslash'); + + $redirect->save(); + + return $message; +} + +/** + * Creates the new default redirect view. + */ +function redirect_update_8103() { + $message = NULL; + + // Only create if the redirect view doesn't exist and views is enabled. + if (!View::load('redirect') && \Drupal::moduleHandler()->moduleExists('views')) { + $config_path = drupal_get_path('module', 'redirect') . '/config/install/views.view.redirect.yml'; + $data = Yaml::parse($config_path); + \Drupal::configFactory()->getEditable('views.view.redirect')->setData($data)->save(TRUE); + $message = 'The new redirect view has been created.'; + } + else { + $message = 'Not creating a redirect view since it already exists.'; + } + return $message; +} + +/** + * Save the bulk delete action to config. + */ +function redirect_update_8104() { + if (!Action::load('redirect_delete_action')) { + $entity_type_manager = \Drupal::entityTypeManager(); + $module_handler = \Drupal::moduleHandler(); + + // Save the bulk delete action to config. + $config_install_path = $module_handler->getModule('redirect')->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY; + $storage = new FileStorage($config_install_path); + $entity_type_manager + ->getStorage('action') + ->create($storage->read('system.action.redirect_delete_action')) + ->trustData() + ->save(); + } +} + +/** + * Ensure to use the redirect_404 submodule. + */ +function redirect_update_8105() { + \Drupal::service('module_installer')->install(['redirect_404']); +} + +/** + * Removes unnecessary settings from storage. + */ +function redirect_update_8106() { + $config = \Drupal::configFactory()->getEditable('redirect.settings'); + $config->set('route_normalizer_enabled', $config->get('normalize_aliases')); + $config->clear('term_path_handler'); + $config->clear('normalize_aliases'); + $config->clear('deslash'); + $config->clear('frontpage_redirect'); + $config->clear('nonclean_to_clean'); + $config->save(); +} + +/** + * Update permissions. + * + * When splitting the permission for settings from the permission to view + * the list of redirects, maintain the current permissions for sites which + * already have this module installed. + */ +function redirect_update_8107() { + if ($roles = Role::loadMultiple()) { + foreach ($roles as $role) { + if ($role->hasPermission('administer redirects')) { + $role->grantPermission('administer redirect settings'); + $role->save(); + } + } + } +} + +/** + * Removes unnecessary settings. + */ +function redirect_update_8108() { + $config = \Drupal::configFactory()->getEditable('redirect.settings'); + $config->clear('canonical'); + $config->clear('content_location_header'); + $config->save(); +} diff --git a/sites/all/modules/contrib/admin/redirect/redirect.libraries.yml b/sites/all/modules/contrib/admin/redirect/redirect.libraries.yml new file mode 100644 index 000000000..987487264 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.libraries.yml @@ -0,0 +1,5 @@ +drupal.redirect.admin: + version: VERSION + css: + component: + css/redirect.admin.css: {} diff --git a/sites/all/modules/contrib/admin/redirect/redirect.links.action.yml b/sites/all/modules/contrib/admin/redirect/redirect.links.action.yml new file mode 100644 index 000000000..3952ca594 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.links.action.yml @@ -0,0 +1,5 @@ +redirect.add: + route_name: redirect.add + title: 'Add redirect' + appears_on: + - 'redirect.list' diff --git a/sites/all/modules/contrib/admin/redirect/redirect.links.menu.yml b/sites/all/modules/contrib/admin/redirect/redirect.links.menu.yml new file mode 100644 index 000000000..b034d47fd --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.links.menu.yml @@ -0,0 +1,26 @@ +redirect.list: + title: 'URL redirects' + parent: system.admin_config_search + description: 'Redirect users from one URL to another.' + route_name: redirect.list + menu_name: admin + +redirect.add: + title: 'Add redirect' + parent: redirect.list + route_name: redirect.add + menu_name: admin + +redirect.settings: + title: 'Settings' + parent: redirect.list + route_name: redirect.settings + description: 'Configure behavior for URL redirects.' + menu_name: admin + +#redirect.devel_generate: +# title: 'Generate redirects' +# parent: dblog.page_not_found +# route_name: redirect.devel_generate +# description: 'Generate a given number of redirects. Optionally delete current redirects.' +# menu_name: admin diff --git a/sites/all/modules/contrib/admin/redirect/redirect.links.task.yml b/sites/all/modules/contrib/admin/redirect/redirect.links.task.yml new file mode 100644 index 000000000..4216cbc30 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.links.task.yml @@ -0,0 +1,10 @@ +redirect.list: + route_name: redirect.list + base_route: redirect.list + title: URL Redirects + +redirect.settings: + route_name: redirect.settings + base_route: redirect.list + title: Settings + weight: 50 diff --git a/sites/all/modules/contrib/admin/redirect/redirect.module b/sites/all/modules/contrib/admin/redirect/redirect.module new file mode 100644 index 000000000..f7a241b92 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.module @@ -0,0 +1,497 @@ + 'redirect')); +} + +/** + * Implements hook_help(). + */ +function redirect_help($route_name, RouteMatchInterface $route_match) { + $output = ''; + switch ($route_name) { + case 'help.page.redirect': + $output = '

' . t('About') . '

'; + $output .= '

' . t('The Redirect module allows users to redirect from old URLs to new URLs. For more information, see the online documentation for Redirect.', [':online' => 'https://www.drupal.org/documentation/modules/path-redirect']) . '

'; + $output .= '
'; + $output .= '

' . t('Uses') . '

'; + $output .= '
' . t('Redirect is accessed from three tabs that help you manage URL Redirects.', [':list' => Url::fromRoute('redirect.list')->toString()]) . '
'; + $output .= '
' . t('Manage URL Redirects') . '
'; + $output .= '
' . t('The "URL Redirects" page is used to setup and manage URL Redirects. New redirects are created here using the Add redirect button which presents a form to simplify the creation of redirects . The URL redirects page provides a list of all redirects on the site and allows you to edit them.', [':redirect' => Url::fromRoute('redirect.list')->toString(), ':add_form' => Url::fromRoute('redirect.add')->toString()]) . '
'; + if (\Drupal::moduleHandler()->moduleExists('redirect_404')) { + $output .= '
' . t('Fix 404 pages') . '
'; + $output .= '
' . t('"Fix 404 pages" lists all paths that have resulted in 404 errors and do not yet have any redirects assigned to them. This 404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested.', [':fix_404' => Url::fromRoute('redirect_404.fix_404')->toString()]) . '
'; + } + elseif (!\Drupal::moduleHandler()->moduleExists('redirect_404') && \Drupal::currentUser()->hasPermission('administer modules')) { + $output .= '
' . t('Fix 404 pages') . '
'; + $output .= '
' . t('404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested. Please install the Redirect 404 submodule to be able to log all paths that have resulted in 404 errors.', [':extend' => Url::fromRoute('system.modules_list')->toString()]) . '
'; + } + $output .= '
' . t('Configure Global Redirects') . '
'; + $output .= '
' . t('The "Settings" page presents you with a number of means to adjust redirect settings.', [':settings' => Url::fromRoute('redirect.settings')->toString()]) . '
'; + $output .= '
'; + return $output; + break; + } +} + +/** + * Implements hook_entity_delete(). + * + * Will delete redirects based on the entity URL. + */ +function redirect_entity_delete(EntityInterface $entity) { + if ($entity->getEntityType()->hasLinkTemplate('canonical')) { + redirect_delete_by_path('internal:/' . $entity->toUrl('canonical')->getInternalPath()); + redirect_delete_by_path('entity:' . $entity->getEntityTypeId() . '/' . $entity->id()); + } +} + +/** + * Implements hook_path_update(). + * + * Will create redirect from the old path alias to the new one. + */ +function redirect_path_update(array $path) { + if (!\Drupal::config('redirect.settings')->get('auto_redirect')) { + return; + } + $original_path = $path['original']; + + // Delete all redirects having the same source as this alias. + redirect_delete_by_path($path['alias'], $path['langcode'], FALSE); + if ($original_path['alias'] != $path['alias']) { + if (!redirect_repository()->findMatchingRedirect($original_path['alias'], array(), $original_path['langcode'])) { + $redirect = Redirect::create(); + $redirect->setSource($original_path['alias']); + $redirect->setRedirect($path['source']); + $redirect->setLanguage($original_path['langcode']); + $redirect->setStatusCode(\Drupal::config('redirect.settings')->get('default_status_code')); + $redirect->save(); + } + } +} + +/** + * Implements hook_path_insert(). + */ +function redirect_path_insert(array $path) { + // Delete all redirects having the same source as this alias. + redirect_delete_by_path($path['alias'], $path['langcode'], FALSE); +} + +/** + * Implements hook_path_delete(). + */ +function redirect_path_delete($path) { + if (!\Drupal::config('redirect.settings')->get('auto_redirect')) { + return; + } + elseif (isset($path['redirect']) && !$path['redirect']) { + return; + } + elseif (empty($path)) { + // @todo Remove this condition and allow $path to use an array type hint + // when http://drupal.org/node/1025904 is fixed. + return; + } + + // Redirect from a deleted alias to the system path. + //if (!redirect_load_by_source($path['alias'], $path['language'])) { + // $redirect = new stdClass(); + // redirect_create($redirect); + // $redirect->source = $path['alias']; + // $redirect->redirect = $path['source']; + // $redirect->language = $path['language']; + // redirect_save($redirect); + //} +} + +/** + * Implements hook_page_build(). + * + * Adds an action on 404 pages to create a redirect. + * + * @todo hook_page_build() can no longer be used for this. Find a different way. + */ +function redirect_page_build(&$page) { + if (redirect_is_current_page_404() && \Drupal::currentUser()->hasPermission('administer redirects')) { + if (!isset($page['content']['system_main']['actions'])) { + $page['content']['system_main']['actions'] = array( + '#theme' => 'links', + '#links' => array(), + '#attributes' => array('class' => array('action-links')), + '#weight' => -100, + ); + } + // We cannot simply use current_path() because if a 404 path is set, then + // that value overrides whatever is in $_GET['q']. The + // drupal_deliver_html_page() function thankfully puts the original current + // path into $_GET['destination']. + $destination = drupal_get_destination(); + $page['content']['system_main']['actions']['#links']['add_redirect'] = array( + 'title' => t('Add URL redirect from this page to another location'), + 'href' => 'admin/config/search/redirect/add', + 'query' => array('source' => $destination['destination']) + drupal_get_destination(), + ); + } +} + +/** + * Gets the redirect repository service. + * + * @return \Drupal\redirect\RedirectRepository + * The repository service. + */ +function redirect_repository() { + return \Drupal::service('redirect.repository'); +} + +/** + * Delete any redirects associated with a path or any of its sub-paths. + * + * Given a source like 'node/1' this function will delete any redirects that + * have that specific source or any sources that match 'node/1/%'. + * + * @param string $path + * An string with an internal Drupal path. + * @param string $langcode + * (optional) If specified, limits deletion to redirects for the given + * language. Defaults to all languages. + * @param bool $match_subpaths_and_redirect + * (optional) Whether redirects with a destination to the given path and + * sub-paths should also be deleted. + * + * @ingroup redirect_api + */ +function redirect_delete_by_path($path, $langcode = NULL, $match_subpaths_and_redirect = TRUE) { + $path = ltrim($path, '/'); + $database = \Drupal::database(); + $query = $database->select('redirect'); + $query->addField('redirect', 'rid'); + $query_or = db_or(); + $query_or->condition('redirect_source__path', $database->escapeLike($path), 'LIKE'); + if ($match_subpaths_and_redirect) { + $query_or->condition('redirect_source__path', $database->escapeLike($path . '/') . '%', 'LIKE'); + $query_or->condition('redirect_redirect__uri', $database->escapeLike($path), 'LIKE'); + $query_or->condition('redirect_redirect__uri', $database->escapeLike($path . '/') . '%', 'LIKE'); + } + + if ($langcode) { + $query->condition('language', $langcode); + } + + $query->condition($query_or); + $rids = $query->execute()->fetchCol(); + + if ($rids) { + foreach (redirect_repository()->loadMultiple($rids) as $redirect) { + $redirect->delete(); + } + } +} + +/** + * Sort an array recusively. + * + * @param $array + * The array to sort, by reference. + * @param $callback + * The sorting callback to use (e.g. 'sort', 'ksort', 'asort'). + * + * @return + * TRUE on success or FALSE on failure. + */ +function redirect_sort_recursive(&$array, $callback = 'sort') { + $result = $callback($array); + foreach ($array as $key => $value) { + if (is_array($value)) { + $result &= redirect_sort_recursive($array[$key], $callback); + } + } + return $result; +} + +/** + * Build the URL of a redirect for display purposes only. + */ +function redirect_url($path, array $options = array(), $clean_url = NULL) { + // @todo - deal with removal of clean_url config. See + // https://drupal.org/node/1659580 + if (!isset($clean_url)) { + //$clean_url = variable_get('clean_url', 0); + } + + if ($path == '') { + $path = ''; + } + + if (!isset($options['alter']) || !empty($options['alter'])) { + \Drupal::moduleHandler()->alter('redirect_url', $path, $options); + } + + // The base_url might be rewritten from the language rewrite in domain mode. + if (!isset($options['base_url'])) { + // @todo - is this correct? See https://drupal.org/node/1798832. + if (isset($options['https']) && Settings::get('mixed_mode_sessions', FALSE)) { + if ($options['https'] === TRUE) { + $options['base_url'] = $GLOBALS['base_secure_url']; + $options['absolute'] = TRUE; + } + elseif ($options['https'] === FALSE) { + $options['base_url'] = $GLOBALS['base_insecure_url']; + $options['absolute'] = TRUE; + } + } + else { + $options['base_url'] = $GLOBALS['base_url']; + } + } + + if (empty($options['absolute']) || url_is_external($path)) { + $url = $path; + } + else { + $url = $options['base_url'] . base_path() . $path; + } + + if (isset($options['query'])) { + $url .= $clean_url ? '?' : '&'; + $url .= UrlHelper::buildQuery($options['query']); + } + if (isset($options['fragment'])) { + $url .= '#' . $options['fragment']; + } + + return $url; +} + +function redirect_status_code_options($code = NULL) { + $codes = array( + 300 => t('300 Multiple Choices'), + 301 => t('301 Moved Permanently'), + 302 => t('302 Found'), + 303 => t('303 See Other'), + 304 => t('304 Not Modified'), + 305 => t('305 Use Proxy'), + 307 => t('307 Temporary Redirect'), + ); + return isset($codes[$code]) ? $codes[$code] : $codes; +} + +/** + * Returns if the current page request is a page not found (404 status error). + * + * Why the fuck do we have to do this? Why is there not an easier way??? + * + * @return + * TRUE if the current page is a 404, or FALSE otherwise. + */ +function redirect_is_current_page_404() { + return drupal_get_http_header('Status') == '404 Not Found'; +} + +/** + * uasort callback; Compare redirects based on language neutrality and rids. + */ +function _redirect_uasort($a, $b) { + $a_weight = isset($a->weight) ? $a->weight : 0; + $b_weight = isset($b->weight) ? $b->weight : 0; + if ($a_weight != $b_weight) { + // First sort by weight (case sensitivity). + return $a_weight > $b_weight; + } + elseif ($a->language != $b->language) { + // Then sort by language specific over language neutral. + return $a->language == Language::LANGCODE_NOT_SPECIFIED; + } + elseif (!empty($a->source_options['query']) != !empty($b->source_options['query'])) { + // Then sort by redirects that do not have query strings over ones that do. + return empty($a->source_options['query']); + } + else { + // Lastly sort by the highest redirect ID. + return $a->rid < $b->rid; + } +} + +/** + * Implements hook_form_FORM_ID_alter() on behalf of locale.module. + */ +function locale_form_redirect_edit_form_alter(array &$form, FormStateInterface $form_state) { + $form['language'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#options' => array(Language::LANGCODE_NOT_SPECIFIED => t('All languages')) + \Drupal::languageManager()->getLanguages(), + '#default_value' => $form['language']['#value'], + '#description' => t('A redirect set for a specific language will always be used when requesting this page in that language, and takes precedence over redirects set for All languages.'), + ); +} + +/** + * Fetch an array of redirect bulk operations. + * + * @see hook_redirect_operations() + * @see hook_redirect_operations_alter() + */ +function redirect_get_redirect_operations() { + $operations = &drupal_static(__FUNCTION__); + + if (!isset($operations)) { + $operations = \Drupal::moduleHandler()->invokeAll('redirect_operations'); + \Drupal::moduleHandler()->alter('redirect_operations', $operations); + } + + return $operations; +} + +/** + * Implements hook_redirect_operations(). + */ +function redirect_redirect_operations() { + $operations['delete'] = array( + 'action' => t('Delete'), + 'action_past' => t('Deleted'), + 'callback' => 'redirect_delete_multiple', + 'confirm' => TRUE, + ); + return $operations; +} + +/** + * Ajax callback for the redirect link widget. + */ +function redirect_source_link_get_status_messages(array $form, FormStateInterface $form_state) { + return $form['redirect_source']['widget'][0]['status_box']; +} + +/** + * Implements hook_entity_extra_field_info(). + */ +function redirect_entity_extra_field_info() { + $extra = []; + + if (\Drupal::service('module_handler')->moduleExists('node')) { + $node_types = \Drupal::entityTypeManager() + ->getStorage('node_type') + ->loadMultiple(); + + foreach ($node_types as $node_type) { + $extra['node'][$node_type->id()]['form']['url_redirects'] = [ + 'label' => t('URL redirects'), + 'description' => t('Redirect module form elements'), + 'weight' => 50, + ]; + } + } + + return $extra; +} + +/** + * Implements hook_form_node_form_alter(). + */ +function redirect_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { + /** @var \Drupal\node\NodeInterface $node */ + $node = $form_state->getFormObject()->getEntity(); + if (!$node->isNew() && \Drupal::currentUser()->hasPermission('administer redirects')) { + + $nid = $node->id(); + + // Find redirects to this node. + $redirects = \Drupal::service('redirect.repository') + ->findByDestinationUri(["internal:/node/$nid", "entity:node/$nid"]); + + // Assemble the rows for the table. + $rows = []; + /** @var \Drupal\Core\Entity\EntityListBuilder $list_builder */ + $list_builder = \Drupal::service('entity.manager')->getListBuilder('redirect'); + /** @var \Drupal\redirect\Entity\Redirect[] $redirects */ + foreach ($redirects as $redirect) { + $row = []; + $path = $redirect->getSourcePathWithQuery(); + $row['path'] = [ + 'class' => ['redirect-table__path'], + 'data' => ['#plain_text' => $path], + 'title' => $path, + ]; + $row['operations'] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $list_builder->getOperations($redirect), + ], + ]; + $rows[] = $row; + } + + // Add the list to the vertical tabs section of the form. + $header = [ + ['class' => ['redirect-table__path'], 'data' => t('From')], + ['class' => ['redirect-table__operations'], 'data' => t('Operations')], + ]; + $form['url_redirects'] = [ + '#type' => 'details', + '#title' => t('URL redirects'), + '#group' => 'advanced', + '#open' => FALSE, + 'table' => [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => t('No URL redirects available.'), + '#attributes' => ['class' => ['redirect-table']], + ], + '#attached' => [ + 'library' => [ + 'redirect/drupal.redirect.admin', + ], + ], + ]; + + if (!empty($rows)) { + $form['url_redirects']['warning'] = [ + '#markup' => t('Note: links open in the current window.'), + '#prefix' => '

', + '#suffix' => '

', + ]; + } + } +} diff --git a/sites/all/modules/contrib/admin/redirect/redirect.permissions.yml b/sites/all/modules/contrib/admin/redirect/redirect.permissions.yml new file mode 100644 index 000000000..8596ee1ca --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.permissions.yml @@ -0,0 +1,4 @@ +administer redirects: + title: 'Administer individual URL redirections' +administer redirect settings: + title: 'Administer global URL redirection settings' diff --git a/sites/all/modules/contrib/admin/redirect/redirect.routing.yml b/sites/all/modules/contrib/admin/redirect/redirect.routing.yml new file mode 100644 index 000000000..4195cbb59 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.routing.yml @@ -0,0 +1,58 @@ +redirect.list: + path: '/admin/config/search/redirect' + defaults: + _entity_list: 'redirect' + _title: 'URL redirects' + requirements: + _permission: 'administer redirects' + +entity.redirect.canonical: + path: '/admin/config/search/redirect/edit/{redirect}' + defaults: + _entity_form: 'redirect.edit' + _title: 'Edit URL redirect' + requirements: + _entity_access: 'redirect.update' + +redirect.add: + path: '/admin/config/search/redirect/add' + defaults: + _entity_form: 'redirect.default' + _title: 'Add URL redirect' + requirements: + _entity_create_access: 'redirect' + +entity.redirect.edit_form: + path: '/admin/config/search/redirect/edit/{redirect}' + defaults: + _entity_form: 'redirect.edit' + _title: 'Edit URL redirect' + requirements: + _entity_access: 'redirect.update' + +entity.redirect.delete_form: + path: '/admin/config/search/redirect/delete/{redirect}' + defaults: + _entity_form: 'redirect.delete' + _title: 'Delete URL redirect' + requirements: + _entity_access: 'redirect.delete' + +entity.redirect.multiple_delete_confirm: + path: '/admin/config/search/redirect/delete' + defaults: + _form: '\Drupal\redirect\Form\RedirectDeleteMultipleForm' + requirements: + _permission: 'administer redirects' + +redirect.settings: + path: '/admin/config/search/redirect/settings' + defaults: + _form: '\Drupal\redirect\Form\RedirectSettingsForm' + _title: 'Settings' + requirements: + _permission: 'administer redirect settings' + +#redirect.devel_generate: +# requirements: +# _module_dependencies: 'devel' diff --git a/sites/all/modules/contrib/admin/redirect/redirect.services.yml b/sites/all/modules/contrib/admin/redirect/redirect.services.yml new file mode 100644 index 000000000..743bd9392 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/redirect.services.yml @@ -0,0 +1,30 @@ +parameters: + route_normalizer_enabled: true +services: + redirect.repository: + class: Drupal\redirect\RedirectRepository + arguments: ['@entity.manager', '@database', '@config.factory'] + tags: + - { name: backend_overridable } + redirect.checker: + class: Drupal\redirect\RedirectChecker + arguments: ['@config.factory', '@state', '@access_manager', '@current_user', '@router.route_provider'] + redirect.request_subscriber: + class: Drupal\redirect\EventSubscriber\RedirectRequestSubscriber + arguments: ['@redirect.repository', '@language_manager', '@config.factory', '@path.alias_manager', '@module_handler', '@entity.manager', '@redirect.checker', '@router.request_context', '@path_processor_manager'] + tags: + - { name: event_subscriber } + redirect.settings_cache_tag: + class: Drupal\redirect\EventSubscriber\RedirectSettingsCacheTag + arguments: ['@cache_tags.invalidator'] + tags: + - { name: event_subscriber } + redirect.route_normalizer_request_subscriber: + class: Drupal\redirect\EventSubscriber\RouteNormalizerRequestSubscriber + arguments: ['@url_generator', '@path.matcher', '@config.factory', '@redirect.checker'] + tags: + - { name: event_subscriber } + redirect.route_subscriber: + class: Drupal\redirect\Routing\RouteSubscriber + tags: + - { name: event_subscriber } diff --git a/sites/all/modules/contrib/admin/redirect/src/Entity/Redirect.php b/sites/all/modules/contrib/admin/redirect/src/Entity/Redirect.php new file mode 100644 index 000000000..ae54ec576 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Entity/Redirect.php @@ -0,0 +1,344 @@ + Unicode::strtolower($source_path), + 'language' => $language, + ); + + if (!empty($source_query)) { + $hash['source_query'] = $source_query; + } + redirect_sort_recursive($hash, 'ksort'); + return Crypt::hashBase64(serialize($hash)); + } + + /** + * {@inheritdoc} + */ + public static function preCreate(EntityStorageInterface $storage_controller, array &$values) { + $values += array( + 'type' => 'redirect', + ); + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage_controller) { + $this->set('hash', Redirect::generateHash($this->redirect_source->path, (array) $this->redirect_source->query, $this->language()->getId())); + } + + /** + * Sets the redirect language. + * + * @param string $language + * Language code. + */ + public function setLanguage($language) { + $this->set('language', $language); + } + + /** + * Sets the redirect status code. + * + * @param int $status_code + * The redirect status code. + */ + public function setStatusCode($status_code) { + $this->set('status_code', $status_code); + } + + /** + * Gets the redirect status code. + * + * @return int + * The redirect status code. + */ + public function getStatusCode() { + return $this->get('status_code')->value; + } + + /** + * Sets the redirect created datetime. + * + * @param int $datetime + * The redirect created datetime. + */ + public function setCreated($datetime) { + $this->set('created', $datetime); + } + + /** + * Gets the redirect created datetime. + * + * @return int + * The redirect created datetime. + */ + public function getCreated() { + return $this->get('created')->value; + } + + /** + * Sets the source URL data. + * + * @param string $path + * The base url of the source. + * @param array $query + * Query arguments. + */ + public function setSource($path, array $query = array()) { + $this->redirect_source->set(0, ['path' => ltrim($path, '/'), 'query' => $query]); + } + + /** + * Gets the source URL data. + * + * @return array + */ + public function getSource() { + return $this->get('redirect_source')->get(0)->getValue(); + } + + /** + * Gets the source base URL. + * + * @return string + */ + public function getSourceUrl() { + return $this->get('redirect_source')->get(0)->getUrl()->toString(); + } + + /** + * Gets the source URL path with its query. + * + * @return string + * The source URL path, eventually with its query. + */ + public function getSourcePathWithQuery() { + $path = '/' . $this->get('redirect_source')->path; + if ($this->get('redirect_source')->query) { + $path .= '?' . UrlHelper::buildQuery($this->get('redirect_source')->query); + } + return $path; + } + + /** + * Gets the redirect URL data. + * + * @return array + * The redirect URL data. + */ + public function getRedirect() { + return $this->get('redirect_redirect')->get(0)->getValue(); + } + + /** + * Sets the redirect destination URL data. + * + * @param string $url + * The base url of the redirect destination. + * @param array $query + * Query arguments. + * @param array $options + * The source url options. + */ + public function setRedirect($url, array $query = array(), array $options = array()) { + $uri = $url . ($query ? '?' . UrlHelper::buildQuery($query) : ''); + $this->redirect_redirect->set(0, ['uri' => 'internal:/' . ltrim($uri, '/'), 'options' => $options]); + } + + /** + * Gets the redirect URL. + * + * @return \Drupal\Core\Url + * The redirect URL. + */ + public function getRedirectUrl() { + return $this->get('redirect_redirect')->get(0)->getUrl(); + } + + /** + * Gets the redirect URL options. + * + * @return array + * The redirect URL options. + */ + public function getRedirectOptions() { + return $this->get('redirect_redirect')->options; + } + + /** + * Gets a specific redirect URL option. + * + * @param string $key + * Option key. + * @param mixed $default + * Default value used in case option does not exist. + * + * @return mixed + * The option value. + */ + public function getRedirectOption($key, $default = NULL) { + $options = $this->getRedirectOptions(); + return isset($options[$key]) ? $options[$key] : $default; + } + + /** + * Gets the current redirect entity hash. + * + * @return string + * The hash. + */ + public function getHash() { + return $this->get('hash')->value; + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields['rid'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Redirect ID')) + ->setDescription(t('The redirect ID.')) + ->setReadOnly(TRUE); + + $fields['uuid'] = BaseFieldDefinition::create('uuid') + ->setLabel(t('UUID')) + ->setDescription(t('The record UUID.')) + ->setReadOnly(TRUE); + + $fields['hash'] = BaseFieldDefinition::create('string') + ->setLabel(t('Hash')) + ->setSetting('max_length', 64) + ->setDescription(t('The redirect hash.')); + + $fields['type'] = BaseFieldDefinition::create('string') + ->setLabel(t('Type')) + ->setDescription(t('The redirect type.')); + + $fields['uid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('User ID')) + ->setDescription(t('The user ID of the node author.')) + ->setDefaultValueCallback('\Drupal\redirect\Entity\Redirect::getCurrentUserId') + ->setSettings(array( + 'target_type' => 'user', + )); + + $fields['redirect_source'] = BaseFieldDefinition::create('redirect_source') + ->setLabel(t('From')) + ->setDescription(t("Enter an internal Drupal path or path alias to redirect (e.g. %example1 or %example2). Fragment anchors (e.g. %anchor) are not allowed.", array('%example1' => 'node/123', '%example2' => 'taxonomy/term/123', '%anchor' => '#anchor'))) + ->setRequired(TRUE) + ->setTranslatable(FALSE) + ->setDisplayOptions('form', array( + 'type' => 'redirect_link', + 'weight' => -5, + )) + ->setDisplayConfigurable('form', TRUE); + + $fields['redirect_redirect'] = BaseFieldDefinition::create('link') + ->setLabel(t('To')) + ->setRequired(TRUE) + ->setTranslatable(FALSE) + ->setSettings(array( + 'link_type' => LinkItemInterface::LINK_GENERIC, + 'title' => DRUPAL_DISABLED + )) + ->setDisplayOptions('form', array( + 'type' => 'link', + 'weight' => -4, + )) + ->setDisplayConfigurable('form', TRUE); + + $fields['language'] = BaseFieldDefinition::create('language') + ->setLabel(t('Language')) + ->setDescription(t('The redirect language.')) + ->setDisplayOptions('form', array( + 'type' => 'language_select', + 'weight' => 2, + )); + + $fields['status_code'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Status code')) + ->setDescription(t('The redirect status code.')) + ->setDefaultValue(0); + + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Created')) + ->setDescription(t('The date when the redirect was created.')); + return $fields; + } + + /** + * Default value callback for 'uid' base field definition. + * + * @see ::baseFieldDefinitions() + * + * @return array + * An array of default values. + */ + public static function getCurrentUserId() { + return array(\Drupal::currentUser()->id()); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectRequestSubscriber.php b/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectRequestSubscriber.php new file mode 100644 index 000000000..3161ccd42 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectRequestSubscriber.php @@ -0,0 +1,201 @@ +redirectRepository = $redirect_repository; + $this->languageManager = $language_manager; + $this->config = $config->get('redirect.settings'); + $this->aliasManager = $alias_manager; + $this->moduleHandler = $module_handler; + $this->entityManager = $entity_manager; + $this->checker = $checker; + $this->context = $context; + $this->pathProcessor = $path_processor; + } + + /** + * Handles the redirect if any found. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The event to process. + */ + public function onKernelRequestCheckRedirect(GetResponseEvent $event) { + // Get a clone of the request. During inbound processing the request + // can be altered. Allowing this here can lead to unexpected behavior. + // For example the path_processor.files inbound processor provided by + // the system module alters both the path and the request; only the + // changes to the request will be propagated, while the change to the + // path will be lost. + $request = clone $event->getRequest(); + + if (!$this->checker->canRedirect($request)) { + return; + } + + // Get URL info and process it to be used for hash generation. + parse_str($request->getQueryString(), $request_query); + + // Do the inbound processing so that for example language prefixes are + // removed. + $path = $this->pathProcessor->processInbound($request->getPathInfo(), $request); + $path = trim($path, '/'); + + $this->context->fromRequest($request); + + try { + $redirect = $this->redirectRepository->findMatchingRedirect($path, $request_query, $this->languageManager->getCurrentLanguage()->getId()); + } + catch (RedirectLoopException $e) { + \Drupal::logger('redirect')->warning($e->getMessage()); + $response = new Response(); + $response->setStatusCode(503); + $response->setContent('Service unavailable'); + $event->setResponse($response); + return; + } + + if (!empty($redirect)) { + + // Handle internal path. + $url = $redirect->getRedirectUrl(); + if ($this->config->get('passthrough_querystring')) { + $url->setOption('query', (array) $url->getOption('query') + $request_query); + } + $headers = [ + 'X-Redirect-ID' => $redirect->id(), + ]; + $response = new TrustedRedirectResponse($url->setAbsolute()->toString(), $redirect->getStatusCode(), $headers); + $response->addCacheableDependency($redirect); + $event->setResponse($response); + } + } + + /** + * Prior to set the response it check if we can redirect. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The event object. + * @param \Drupal\Core\Url $url + * The Url where we want to redirect. + */ + protected function setResponse(GetResponseEvent $event, Url $url) { + $request = $event->getRequest(); + $this->context->fromRequest($request); + + parse_str($request->getQueryString(), $query); + $url->setOption('query', $query); + $url->setAbsolute(TRUE); + + // We can only check access for routed URLs. + if (!$url->isRouted() || $this->checker->canRedirect($request, $url->getRouteName())) { + // Add the 'rendered' cache tag, so that we can invalidate all responses + // when settings are changed. + $response = new TrustedRedirectResponse($url->toString(), 301); + $response->addCacheableDependency(CacheableMetadata::createFromRenderArray([])->addCacheTags(['rendered'])); + $event->setResponse($response); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // This needs to run before RouterListener::onKernelRequest(), which has + // a priority of 32. Otherwise, that aborts the request if no matching + // route is found. + $events[KernelEvents::REQUEST][] = array('onKernelRequestCheckRedirect', 33); + return $events; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectSettingsCacheTag.php b/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectSettingsCacheTag.php new file mode 100644 index 000000000..7dfb063b3 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RedirectSettingsCacheTag.php @@ -0,0 +1,54 @@ +cacheTagsInvalidator = $cache_tags_invalidator; + } + + /** + * Invalidate the 'rendered' cache tag whenever the settings are modified. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + * The Event to process. + */ + public function onSave(ConfigCrudEvent $event) { + // Changing the Redirect settings means that any cached page might + // result in a different response, so we need to invalidate them all. + if ($event->getConfig()->getName() === 'redirect.settings') { + $this->cacheTagsInvalidator->invalidateTags(['rendered']); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[ConfigEvents::SAVE][] = ['onSave']; + return $events; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RouteNormalizerRequestSubscriber.php b/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RouteNormalizerRequestSubscriber.php new file mode 100644 index 000000000..b8eae4df0 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/EventSubscriber/RouteNormalizerRequestSubscriber.php @@ -0,0 +1,149 @@ +urlGenerator = $url_generator; + $this->pathMatcher = $path_matcher; + $this->redirectChecker = $redirect_checker; + $this->config = $config->get('redirect.settings'); + } + + /** + * Performs a redirect if the URL changes in routing. + * + * The redirect happens if a URL constructed from the current route is + * different from the requested one. Examples: + * - Language negotiation system detected a language to use, and that language + * has a path prefix: perform a redirect to the language prefixed URL. + * - A route that's set as the front page is requested: redirect to the front + * page. + * - Requested path has an alias: redirect to alias. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestRedirect(GetResponseEvent $event) { + + if (!$this->config->get('route_normalizer_enabled') || !$event->isMasterRequest()) { + return; + } + + $request = $event->getRequest(); + if ($request->attributes->get('_disable_route_normalizer')) { + return; + } + + if ($this->redirectChecker->canRedirect($request)) { + // The "" placeholder can be used for all routes except the front + // page because it's not a real route. + $route_name = $this->pathMatcher->isFrontPage() ? '' : ''; + + // Don't pass in the query here using $request->query->all() + // since that can potentially modify the query parameters. + $options = ['absolute' => TRUE]; + $redirect_uri = $this->urlGenerator->generateFromRoute($route_name, [], $options); + + // Strip off query parameters added by the route such as a CSRF token. + if (strpos($redirect_uri, '?') !== FALSE) { + $redirect_uri = strtok($redirect_uri, '?'); + } + + // Append back the request query string from $_SERVER. + $query_string = $request->server->get('QUERY_STRING'); + if ($query_string) { + $redirect_uri .= '?' . $query_string; + } + + // Remove /index.php from redirect uri the hard way. + if (!RequestHelper::isCleanUrl($request)) { + // This needs to be fixed differently. + $redirect_uri = str_replace('/index.php', '', $redirect_uri); + } + + $original_uri = $request->getSchemeAndHttpHost() . $request->getRequestUri(); + $original_uri = urldecode($original_uri); + $redirect_uri = urldecode($redirect_uri); + if ($redirect_uri != $original_uri) { + $response = new TrustedRedirectResponse($redirect_uri, $this->config->get('default_status_code')); + $response->headers->set('X-Drupal-Route-Normalizer', 1); + $event->setResponse($response); + // Disable page cache for redirects as that results in unpredictable + // behavior, e.g. when a trailing ? without query parameters is + // involved. + // @todo Remove when https://www.drupal.org/node/2761639 is fixed in + // Drupal core. + \Drupal::service('page_cache_kill_switch')->trigger(); + } + } + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestRedirect', 30); + return $events; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Exception/RedirectLoopException.php b/sites/all/modules/contrib/admin/redirect/src/Exception/RedirectLoopException.php new file mode 100644 index 000000000..f2793f3e8 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Exception/RedirectLoopException.php @@ -0,0 +1,24 @@ + $path, '%rid' => $rid])); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteForm.php b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteForm.php new file mode 100644 index 000000000..ae3c2bcb7 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteForm.php @@ -0,0 +1,41 @@ +t('Are you sure you want to delete the URL redirect from %source to %redirect?', array('%source' => $this->entity->getSourceUrl(), '%redirect' => $this->entity->getRedirectUrl()->toString())); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('redirect.list'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + drupal_set_message(t('The redirect %redirect has been deleted.', array('%redirect' => $this->entity->getRedirectUrl()->toString()))); + $form_state->setRedirect('redirect.list'); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteMultipleForm.php b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteMultipleForm.php new file mode 100644 index 000000000..9e1ed222e --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectDeleteMultipleForm.php @@ -0,0 +1,140 @@ +privateTempStoreFactory = $temp_store_factory; + $this->redirectStorage = $entity_type_manager->getStorage('redirect'); + $this->currentUser = $account; + $this->setStringTranslation($string_translation); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.private_tempstore'), + $container->get('entity_type.manager'), + $container->get('current_user'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'redirect_multiple_delete_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->formatPlural(count($this->redirects), 'Are you sure you want to delete this redirect?', 'Are you sure you want to delete these redirects?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('redirect.list'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $this->redirects = $this->privateTempStoreFactory->get('redirect_multiple_delete_confirm')->get($this->currentUser->id()); + if (empty($this->redirects)) { + return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString()); + } + + $form['redirects'] = [ + '#theme' => 'item_list', + '#items' => array_map(function ($redirect) { + return $redirect->label(); + }, $this->redirects), + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + if ($form_state->getValue('confirm') && !empty($this->redirects)) { + $this->redirectStorage->delete($this->redirects); + $this->privateTempStoreFactory->get('redirect_multiple_delete_confirm')->delete($this->currentUser->id()); + $count = count($this->redirects); + $this->logger('redirect')->notice('Deleted @count redirects.', ['@count' => $count]); + drupal_set_message($this->stringTranslation->formatPlural($count, 'Deleted 1 redirect.', 'Deleted @count redirects.')); + } + $form_state->setRedirect('redirect.list'); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Form/RedirectForm.php b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectForm.php new file mode 100644 index 000000000..573fa99d6 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectForm.php @@ -0,0 +1,152 @@ +entity; + + if ($redirect->isNew()) { + + // To pass in the query set parameters into GET as follows: + // source_query[key1]=value1&source_query[key2]=value2 + $source_query = array(); + if ($this->getRequest()->get('source_query')) { + $source_query = $this->getRequest()->get('source_query'); + } + + $redirect_options = array(); + $redirect_query = array(); + if ($this->getRequest()->get('redirect_options')) { + $redirect_options = $this->getRequest()->get('redirect_options'); + if (isset($redirect_options['query'])) { + $redirect_query = $redirect_options['query']; + unset($redirect_options['query']); + } + } + + $source_url = urldecode($this->getRequest()->get('source')); + if (!empty($source_url)) { + $redirect->setSource($source_url, $source_query); + } + + $redirect_url = urldecode($this->getRequest()->get('redirect')); + if (!empty($redirect_url)) { + try { + $redirect->setRedirect($redirect_url, $redirect_query, $redirect_options); + } + catch (MatchingRouteNotFoundException $e) { + drupal_set_message(t('Invalid redirect URL %url provided.', array('%url' => $redirect_url)), 'warning'); + } + } + + $redirect->setLanguage($this->getRequest()->get('language') ? $this->getRequest()->get('language') : Language::LANGCODE_NOT_SPECIFIED); + } + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + /** @var \Drupal\redirect\Entity\Redirect $redirect */ + $redirect = $this->entity; + + // Only add the configured languages and a single key for all languages. + if (isset($form['language']['widget'][0]['value'])) { + foreach (\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_CONFIGURABLE) as $langcode => $language) { + $form['language']['widget'][0]['value']['#options'][$langcode] = $language->getName(); + } + $form['language']['widget'][0]['value']['#options'][LanguageInterface::LANGCODE_NOT_SPECIFIED] = t('- All languages -'); + } + + $default_code = $redirect->getStatusCode() ? $redirect->getStatusCode() : \Drupal::config('redirect.settings')->get('default_status_code'); + + $form['status_code'] = array( + '#type' => 'select', + '#title' => t('Redirect status'), + '#description' => t('You can find more information about HTTP redirect status codes at @status-codes.', array('@status-codes' => 'http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection')), + '#default_value' => $default_code, + '#options' => redirect_status_code_options(), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + $source = $form_state->getValue(array('redirect_source', 0)); + $redirect = $form_state->getValue(array('redirect_redirect', 0)); + + if ($source['path'] == '') { + $form_state->setErrorByName('redirect_source', t('It is not allowed to create a redirect from the front page.')); + } + if (strpos($source['path'], '#') !== FALSE) { + $form_state->setErrorByName('redirect_source', t('The anchor fragments are not allowed.')); + } + if (strpos($source['path'], '/') === 0) { + $form_state->setErrorByName('redirect_source', t('The url to redirect from should not start with a forward slash (/).')); + } + + try { + $source_url = Url::fromUri('internal:/' . $source['path']); + $redirect_url = Url::fromUri($redirect['uri']); + + // It is relevant to do this comparison only in case the source path has + // a valid route. Otherwise the validation will fail on the redirect path + // being an invalid route. + if ($source_url->toString() == $redirect_url->toString()) { + $form_state->setErrorByName('redirect_redirect', t('You are attempting to redirect the page to itself. This will result in an infinite loop.')); + } + } + catch (\InvalidArgumentException $e) { + // Do nothing, we want to only compare the resulting URLs. + } + + $parsed_url = UrlHelper::parse(trim($source['path'])); + $path = isset($parsed_url['path']) ? $parsed_url['path'] : NULL; + $query = isset($parsed_url['query']) ? $parsed_url['query'] : NULL; + $hash = Redirect::generateHash($path, $query, $form_state->getValue('language')[0]['value']); + + // Search for duplicate. + $redirects = \Drupal::entityManager() + ->getStorage('redirect') + ->loadByProperties(array('hash' => $hash)); + + if (!empty($redirects)) { + $redirect = array_shift($redirects); + if ($this->entity->isNew() || $redirect->id() != $this->entity->id()) { + $form_state->setErrorByName('redirect_source', t('The source path %source is already being redirected. Do you want to edit the existing redirect?', + array( + '%source' => $source['path'], + '@edit-page' => $redirect->url('edit-form')))); + } + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $this->entity->save(); + drupal_set_message(t('The redirect has been saved.')); + $form_state->setRedirect('redirect.list'); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Form/RedirectSettingsForm.php b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectSettingsForm.php new file mode 100644 index 000000000..f2b7d1b2f --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Form/RedirectSettingsForm.php @@ -0,0 +1,93 @@ +config('redirect.settings'); + $form['redirect_auto_redirect'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Automatically create redirects when URL aliases are changed.'), + '#default_value' => $config->get('auto_redirect'), + '#disabled' => !\Drupal::moduleHandler()->moduleExists('path'), + ); + $form['redirect_passthrough_querystring'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Retain query string through redirect.'), + '#default_value' => $config->get('passthrough_querystring'), + '#description' => $this->t('For example, given a redirect from %source to %redirect, if a user visits %sourcequery they would be redirected to %redirectquery. The query strings in the redirection will always take precedence over the current query string.', array('%source' => 'source-path', '%redirect' => 'node?a=apples', '%sourcequery' => 'source-path?a=alligators&b=bananas', '%redirectquery' => 'node?a=apples&b=bananas')), + ); + $form['redirect_warning'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Display a warning message to users when they are redirected.'), + '#default_value' => $config->get('warning'), + '#access' => FALSE, + ); + $form['redirect_default_status_code'] = array( + '#type' => 'select', + '#title' => $this->t('Default redirect status'), + '#description' => $this->t('You can find more information about HTTP redirect status codes at @status-codes.', array('@status-codes' => 'http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection')), + '#options' => redirect_status_code_options(), + '#default_value' => $config->get('default_status_code'), + ); + $form['globals'] = array( + '#type' => 'fieldset', + '#title' => $this->t('Global redirects'), + '#description' => $this->t('(formerly Global Redirect features)'), + ); + $form['globals']['redirect_route_normalizer_enabled'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Enforce clean and canonical URLs.'), + '#description' => $this->t('Enabling this will automatically redirect to the canonical URL of any page. That includes redirecting to an alias if existing, removing trainling slashes, ensure the language prefix is set and similar clean-up.'), + '#default_value' => $config->get('route_normalizer_enabled'), + ); + $form['globals']['redirect_ignore_admin_path'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Ignore redirections on admin paths.'), + '#default_value' => $config->get('ignore_admin_path'), + ); + $form['globals']['redirect_access_check'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Check access to the redirected page'), + '#description' => $this->t('This helps to stop redirection on protected pages and avoids giving away secret URL\'s. By default this feature is disabled to avoid any unexpected behavior'), + '#default_value' => $config->get('access_check'), + ); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('redirect.settings'); + foreach ($form_state->getValues() as $key => $value) { + if (strpos($key, 'redirect_') !== FALSE) { + $config->set(str_replace('redirect_', '', $key), $value); + } + } + $config->save(); + drupal_set_message(t('Configuration was saved.')); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Plugin/Action/DeleteRedirect.php b/sites/all/modules/contrib/admin/redirect/src/Plugin/Action/DeleteRedirect.php new file mode 100644 index 000000000..8b7f57972 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Plugin/Action/DeleteRedirect.php @@ -0,0 +1,92 @@ +currentUser = $current_user; + $this->privateTempStore = $temp_store_factory->get('redirect_multiple_delete_confirm'); + + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('user.private_tempstore'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function executeMultiple(array $entities) { + $this->privateTempStore->set($this->currentUser->id(), $entities); + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + $this->executeMultiple([$object]); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + return $object->access('delete', $account, $return_as_object); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldFormatter/RedirectSourceFormatter.php b/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldFormatter/RedirectSourceFormatter.php new file mode 100644 index 000000000..7ef400de2 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldFormatter/RedirectSourceFormatter.php @@ -0,0 +1,36 @@ + $item) { + $elements[$delta] = array( + '#markup' => urldecode($item->getUrl()->toString()), + ); + } + + return $elements; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldType/RedirectSourceItem.php b/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldType/RedirectSourceItem.php new file mode 100644 index 000000000..53a0efc66 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldType/RedirectSourceItem.php @@ -0,0 +1,112 @@ +setLabel(t('Path')); + + $properties['query'] = MapDataDefinition::create() + ->setLabel(t('Query')); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + return array( + 'columns' => array( + 'path' => array( + 'description' => 'The source path', + 'type' => 'varchar', + 'length' => 2048, + ), + 'query' => array( + 'description' => 'Serialized array of path queries', + 'type' => 'blob', + 'size' => 'big', + 'serialize' => TRUE, + ), + ), + 'indexes' => array( + 'path' => array(array('path', 50)), + ), + ); + } + + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + // Set random length for the path. + $domain_length = mt_rand(7, 15); + $random = new Random(); + + $values['path'] = 'http://www.' . $random->word($domain_length); + + return $values; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return $this->path === NULL || $this->path === ''; + } + + /** + * {@inheritdoc} + */ + public static function mainPropertyName() { + return 'path'; + } + + /** + * {@inheritdoc} + */ + public function setValue($values, $notify = TRUE) { + // Unserialize the values. + // @todo The storage controller should take care of this, see + // SqlContentEntityStorage::loadFieldItems, see + // https://www.drupal.org/node/2414835 + if (isset($values['query']) && is_string($values['query'])) { + $values['query'] = unserialize($values['query']); + } + parent::setValue($values, $notify); + } + + /** + * {@inheritdoc} + */ + public function getUrl() { + return Url::fromUri('base:' . $this->path, ['query' => $this->query]); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldWidget/RedirectSourceWidget.php b/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldWidget/RedirectSourceWidget.php new file mode 100644 index 000000000..729abe45a --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Plugin/Field/FieldWidget/RedirectSourceWidget.php @@ -0,0 +1,117 @@ +path; + if ($items[$delta]->query) { + $default_url_value .= '?' . http_build_query($items[$delta]->query); + } + $element['path'] = array( + '#type' => 'textfield', + '#title' => $this->t('Path'), + '#placeholder' => $this->getSetting('placeholder_url'), + '#default_value' => $default_url_value, + '#maxlength' => 2048, + '#required' => $element['#required'], + '#field_prefix' => Url::fromRoute('', array(), array('absolute' => TRUE))->toString(), + '#attributes' => array('data-disable-refocus' => 'true'), + ); + + // If creating new URL add checks. + if ($items->getEntity()->isNew()) { + $element['status_box'] = array( + '#prefix' => '', + ); + + $source_path = $form_state->getValue(array('redirect_source', 0, 'path')); + if ($source_path) { + $source_path = trim($source_path); + + // Warning about creating a redirect from a valid path. + // @todo - Hmm... exception driven logic. Find a better way how to + // determine if we have a valid path. + try { + \Drupal::service('router')->match('/' . $form_state->getValue(array('redirect_source', 0, 'path'))); + $element['status_box'][]['#markup'] = '
' . t('The source path %path is likely a valid path. It is preferred to create URL aliases for existing paths rather than redirects.', + array('%path' => $source_path, '@url-alias' => Url::fromRoute('path.admin_add')->toString())) . '
'; + } + catch (ResourceNotFoundException $e) { + // Do nothing, expected behaviour. + } + + // Warning about the path being already redirected. + $parsed_url = UrlHelper::parse($source_path); + $path = isset($parsed_url['path']) ? $parsed_url['path'] : NULL; + if (!empty($path)) { + /** @var \Drupal\redirect\RedirectRepository $repository */ + $repository = \Drupal::service('redirect.repository'); + $redirects = $repository->findBySourcePath($path); + if (!empty($redirects)) { + $redirect = array_shift($redirects); + $element['status_box'][]['#markup'] = '
' . t('The base source path %source is already being redirected. Do you want to edit the existing redirect?', array('%source' => $source_path, '@edit-page' => $redirect->url('edit-form'))) . '
'; + } + } + } + + $element['path']['#ajax'] = array( + 'callback' => 'redirect_source_link_get_status_messages', + 'wrapper' => 'redirect-link-status', + ); + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + $values = parent::massageFormValues($values, $form, $form_state); + // It is likely that the url provided for this field is not existing and + // so the logic in the parent method did not set any defaults. Just run + // through all url values and add defaults. + foreach ($values as &$value) { + if (!empty($value['path'])) { + // In case we have query process the url. + if (strpos($value['path'], '?') !== FALSE) { + $url = UrlHelper::parse($value['path']); + $value['path'] = $url['path']; + $value['query'] = $url['query']; + } + } + } + return $values; + } +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Plugin/Validation/Constraint/SourceLinkTypeConstraint.php b/sites/all/modules/contrib/admin/redirect/src/Plugin/Validation/Constraint/SourceLinkTypeConstraint.php new file mode 100644 index 000000000..5d354a07d --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Plugin/Validation/Constraint/SourceLinkTypeConstraint.php @@ -0,0 +1,92 @@ +context = $context; + } + + /** + * {@inheritdoc} + */ + public function validatedBy() { + return get_class($this); + } + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + if (isset($value)) { + $url_is_valid = TRUE; + /** @var $link_item \Drupal\link\LinkItemInterface */ + $link_item = $value; + $link_type = $link_item->getFieldDefinition()->getSetting('link_type'); + $url_string = $link_item->url; + // Validate the url property. + if ($url_string !== '') { + try { + // @todo This shouldn't be needed, but massageFormValues() may not + // run. + $parsed_url = UrlHelper::parse($url_string); + + if (!empty($parsed_url['path'])) { + $url = Url::fromUri('internal:' . $parsed_url['path']); + + if ($url->isExternal() && !UrlHelper::isValid($url_string, TRUE)) { + $url_is_valid = FALSE; + } + elseif ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) { + $url_is_valid = FALSE; + } + } + } + catch (NotFoundHttpException $e) { + $url_is_valid = FALSE; + } + catch (ResourceNotFoundException $e) { + // User is creating a redirect from non existing path. This is not an + // error state. + $url_is_valid = TRUE; + } + catch (ParamNotConvertedException $e) { + $url_is_valid = FALSE; + } + } + if (!$url_is_valid) { + $this->context->addViolation($this->message, array('%url' => $url_string)); + } + } + } +} + diff --git a/sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/process/PathRedirect.php b/sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/process/PathRedirect.php new file mode 100644 index 000000000..ecff15446 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/process/PathRedirect.php @@ -0,0 +1,42 @@ +select('path_redirect', 'p') + ->fields('p', array( + 'rid', + 'source', + 'redirect', + 'query', + 'fragment', + 'language', + 'type', + 'last_used', + )); + + return $query; + } + + /** + * {@inheritdoc} + */ + public function fields() { + $fields = array( + 'rid' => $this->t('Redirect ID'), + 'source' => $this->t('Source'), + 'redirect' => $this->t('Redirect'), + 'query' => $this->t('Query'), + 'fragment' => $this->t('Fragment'), + 'language' => $this->t('Language'), + 'type' => $this->t('Type'), + 'last_used' => $this->t('Last Used'), + ); + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['rid']['type'] = 'integer'; + return $ids; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/source/d7/PathRedirect.php b/sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/source/d7/PathRedirect.php new file mode 100644 index 000000000..c9ec720d6 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Plugin/migrate/source/d7/PathRedirect.php @@ -0,0 +1,81 @@ +select('redirect', 'p')->fields('p'); + + return $query; + } + + /** + * {@inheritdoc} + */ + public function prepareRow(Row $row) { + static $default_status_code; + if (!isset($default_status_code)) { + $default_status_code = unserialize($this->getDatabase() + ->select('variable', 'v') + ->fields('v', ['value']) + ->condition('name', 'redirect_default_status_code') + ->execute() + ->fetchField()); + } + $current_status_code = $row->getSourceProperty('status_code'); + $status_code = $current_status_code != 0 ? $current_status_code : $default_status_code; + $row->setSourceProperty('status_code', $status_code); + return parent::prepareRow($row); + } + + /** + * {@inheritdoc} + */ + public function fields() { + $fields = [ + 'rid' => $this->t('Redirect ID'), + 'hash' => $this->t('Hash'), + 'type' => $this->t('Type'), + 'uid' => $this->t('UID'), + 'source' => $this->t('Source'), + 'source_options' => $this->t('Source Options'), + 'redirect' => $this->t('Redirect'), + 'redirect_options' => $this->t('Redirect Options'), + 'language' => $this->t('Language'), + 'status_code' => $this->t('Status Code'), + 'count' => $this->t('Count'), + 'access' => $this->t('Access'), + ]; + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['rid']['type'] = 'integer'; + return $ids; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/RedirectChecker.php b/sites/all/modules/contrib/admin/redirect/src/RedirectChecker.php new file mode 100644 index 000000000..02da7ad2e --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/RedirectChecker.php @@ -0,0 +1,98 @@ +config = $config->get('redirect.settings'); + $this->accessManager = $access_manager; + $this->state = $state; + $this->account = $account; + $this->routeProvider = $route_provider; + } + + /** + * Determines if redirect may be performed. + * + * @param Request $request + * The current request object. + * @param string $route_name + * The current route name. + * + * @return bool + * TRUE if redirect may be performed. + */ + public function canRedirect(Request $request, $route_name = NULL) { + $can_redirect = TRUE; + if (isset($route_name)) { + $route = $this->routeProvider->getRouteByName($route_name); + if ($this->config->get('access_check')) { + // Do not redirect if is a protected page. + $can_redirect = $this->accessManager->checkNamedRoute($route_name, [], $this->account); + } + } + else { + $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT); + } + + if (!preg_match('/index\.php$/', $request->getScriptName())) { + // Do not redirect if the root script is not /index.php. + $can_redirect = FALSE; + } + elseif (!($request->isMethod('GET') || $request->isMethod('HEAD'))) { + // Do not redirect if this is other than GET request. + $can_redirect = FALSE; + } + elseif ($this->state->get('system.maintenance_mode') || defined('MAINTENANCE_MODE')) { + // Do not redirect in offline or maintenance mode. + $can_redirect = FALSE; + } + elseif ($request->query->has('destination')) { + $can_redirect = FALSE; + } + elseif ($this->config->get('ignore_admin_path') && isset($route)) { + // Do not redirect on admin paths. + $can_redirect &= !(bool) $route->getOption('_admin_route'); + } + + return $can_redirect; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/RedirectRepository.php b/sites/all/modules/contrib/admin/redirect/src/RedirectRepository.php new file mode 100644 index 000000000..f8a1cea9a --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/RedirectRepository.php @@ -0,0 +1,178 @@ +manager = $manager; + $this->connection = $connection; + $this->config = $config_factory->get('redirect.settings'); + } + + /** + * Gets a redirect for given path, query and language. + * + * @param string $source_path + * The redirect source path. + * @param array $query + * The redirect source path query. + * @param $language + * The language for which is the redirect. + * + * @return \Drupal\redirect\Entity\Redirect + * The matched redirect entity. + * + * @throws \Drupal\redirect\Exception\RedirectLoopException + */ + public function findMatchingRedirect($source_path, array $query = [], $language = Language::LANGCODE_NOT_SPECIFIED) { + $source_path = ltrim($source_path, '/'); + $hashes = [Redirect::generateHash($source_path, $query, $language)]; + if ($language != Language::LANGCODE_NOT_SPECIFIED) { + $hashes[] = Redirect::generateHash($source_path, $query, Language::LANGCODE_NOT_SPECIFIED); + } + + // Add a hash without the query string if using passthrough querystrings. + if (!empty($query) && $this->config->get('passthrough_querystring')) { + $hashes[] = Redirect::generateHash($source_path, [], $language); + if ($language != Language::LANGCODE_NOT_SPECIFIED) { + $hashes[] = Redirect::generateHash($source_path, [], Language::LANGCODE_NOT_SPECIFIED); + } + } + + // Load redirects by hash. A direct query is used to improve performance. + $rid = $this->connection->query('SELECT rid FROM {redirect} WHERE hash IN (:hashes[]) ORDER BY LENGTH(redirect_source__query) DESC', [':hashes[]' => $hashes])->fetchField(); + + if (!empty($rid)) { + // Check if this is a loop. + if (in_array($rid, $this->foundRedirects)) { + throw new RedirectLoopException('/' . $source_path, $rid); + } + $this->foundRedirects[] = $rid; + + $redirect = $this->load($rid); + + // Find chained redirects. + if ($recursive = $this->findByRedirect($redirect, $language)) { + // Reset found redirects. + $this->foundRedirects = []; + return $recursive; + } + + return $redirect; + } + + return NULL; + } + + /** + * Helper function to find recursive redirects. + * + * @param \Drupal\redirect\Entity\Redirect + * The redirect object. + * @param string $language + * The language to use. + */ + protected function findByRedirect(Redirect $redirect, $language) { + $uri = $redirect->getRedirectUrl(); + $baseUrl = \Drupal::request()->getBaseUrl(); + $path = ltrim(substr($uri->toString(), strlen($baseUrl)), '/'); + $query = $uri->getOption('query') ?: []; + return $this->findMatchingRedirect($path, $query, $language); + } + + /** + * Finds redirects based on the source path. + * + * @param string $source_path + * The redirect source path (without the query). + * + * @return \Drupal\redirect\Entity\Redirect[] + * Array of redirect entities. + */ + public function findBySourcePath($source_path) { + $ids = $this->manager->getStorage('redirect')->getQuery() + ->condition('redirect_source.path', $source_path, 'LIKE') + ->execute(); + return $this->manager->getStorage('redirect')->loadMultiple($ids); + } + + /** + * Finds redirects based on the destination URI. + * + * @param string[] $destination_uri + * List of destination URIs, for example ['internal:/node/123']. + * + * @return \Drupal\redirect\Entity\Redirect[] + * Array of redirect entities. + */ + public function findByDestinationUri(array $destination_uri) { + $storage = $this->manager->getStorage('redirect'); + $ids = $storage->getQuery() + ->condition('redirect_redirect.uri', $destination_uri, 'IN') + ->execute(); + return $storage->loadMultiple($ids); + } + + /** + * Load redirect entity by id. + * + * @param int $redirect_id + * The redirect id. + * + * @return \Drupal\redirect\Entity\Redirect + */ + public function load($redirect_id) { + return $this->manager->getStorage('redirect')->load($redirect_id); + } + + /** + * Loads multiple redirect entities. + * + * @param array $redirect_ids + * Redirect ids to load. + * + * @return \Drupal\redirect\Entity\Redirect[] + * List of redirect entities. + */ + public function loadMultiple(array $redirect_ids = NULL) { + return $this->manager->getStorage('redirect')->loadMultiple($redirect_ids); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/src/RedirectStorageSchema.php b/sites/all/modules/contrib/admin/redirect/src/RedirectStorageSchema.php new file mode 100644 index 000000000..e034362f1 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/RedirectStorageSchema.php @@ -0,0 +1,31 @@ + ['hash'], + ]; + $schema['redirect']['indexes'] += [ + // Limit length to 191. + 'source_language' => [['redirect_source__path', 191], 'language'], + ]; + + return $schema; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Routing/RouteSubscriber.php b/sites/all/modules/contrib/admin/redirect/src/Routing/RouteSubscriber.php new file mode 100644 index 000000000..439093a5f --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Routing/RouteSubscriber.php @@ -0,0 +1,28 @@ +get('image.style_public')) { + $route->setDefault('_disable_route_normalizer', TRUE); + } + if ($route = $collection->get('image.style_private')) { + $route->setDefault('_disable_route_normalizer', TRUE); + } + if ($route = $collection->get('system.files')) { + $route->setDefault('_disable_route_normalizer', TRUE); + } + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Tests/AssertRedirectTrait.php b/sites/all/modules/contrib/admin/redirect/src/Tests/AssertRedirectTrait.php new file mode 100644 index 000000000..ae9368886 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Tests/AssertRedirectTrait.php @@ -0,0 +1,53 @@ +drupalHead($path); + $headers = $this->drupalGetHeaders(TRUE); + + $ending_url = isset($headers[0]['location']) ? $headers[0]['location'] : NULL; + $message = SafeMarkup::format('Testing redirect from %from to %to. Ending url: %url', [ + '%from' => $path, + '%to' => $expected_ending_url, + '%url' => $ending_url, + ]); + + if ($expected_ending_url == '') { + $expected_ending_url = Url::fromUri('base:')->setAbsolute()->toString(); + } + elseif (!empty($expected_ending_url)) { + // Check for absolute/external urls. + if (!parse_url($expected_ending_url, PHP_URL_SCHEME)) { + $expected_ending_url = Url::fromUri('base:' . $expected_ending_url)->setAbsolute()->toString(); + } + } + else { + $expected_ending_url = NULL; + } + + $this->assertEqual($expected_ending_url, $ending_url, $message); + + $this->assertEqual($headers[0][':status'], $expected_ending_status); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Tests/GlobalRedirectTest.php b/sites/all/modules/contrib/admin/redirect/src/Tests/GlobalRedirectTest.php new file mode 100644 index 000000000..c457f82cc --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Tests/GlobalRedirectTest.php @@ -0,0 +1,276 @@ +config = $this->config('redirect.settings'); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + + // Create a users for testing the access. + $this->normalUser = $this->drupalCreateUser([ + 'access content', + 'create page content', + 'create url aliases', + 'access administration pages', + ]); + $this->adminUser = $this->drupalCreateUser([ + 'administer site configuration', + 'access administration pages', + 'administer languages', + 'administer content types', + 'administer content translation', + 'create page content', + 'edit own page content', + 'create content translations', + ]); + + // Save the node. + $this->node = $this->drupalCreateNode([ + 'type' => 'page', + 'title' => 'Test Page Node', + 'path' => ['alias' => '/test-node'], + 'language' => Language::LANGCODE_NOT_SPECIFIED, + ]); + + // Create an alias for the create story path - this is used in the + // "redirect with permissions testing" test. + \Drupal::service('path.alias_storage')->save('/admin/config/system/site-information', '/site-info'); + + // Create a taxonomy term for the forum. + $term = entity_create('taxonomy_term', [ + 'name' => 'Test Forum Term', + 'vid' => 'forums', + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + ]); + $term->save(); + $this->forumTerm = $term; + + // Create another taxonomy vocabulary with a term. + $vocab = entity_create('taxonomy_vocabulary', [ + 'name' => 'test vocab', + 'vid' => 'test-vocab', + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + ]); + $vocab->save(); + $term = entity_create('taxonomy_term', [ + 'name' => 'Test Term', + 'vid' => $vocab->id(), + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + 'path' => ['alias' => '/test-term'], + ]); + $term->save(); + + $this->term = $term; + } + + /** + * Will test the redirects. + */ + public function testRedirects() { + + // First test that the good stuff can be switched off. + $this->config->set('route_normalizer_enabled', FALSE)->save(); + $this->assertRedirect('index.php/node/' . $this->node->id(), NULL, 'HTTP/1.1 200 OK'); + $this->assertRedirect('index.php/test-node', NULL, 'HTTP/1.1 200 OK'); + $this->assertRedirect('test-node/', NULL, 'HTTP/1.1 200 OK'); + $this->assertRedirect('Test-node/', NULL, 'HTTP/1.1 200 OK'); + + $this->config->set('route_normalizer_enabled', TRUE)->save(); + + // Test alias normalization. + $this->assertRedirect('node/' . $this->node->id(), 'test-node'); + $this->assertRedirect('Test-node', 'test-node'); + + // Test redirects for non-clean urls. + $this->assertRedirect('index.php/node/' . $this->node->id(), 'test-node'); + $this->assertRedirect('index.php/test-node', 'test-node'); + + // Test deslashing. + $this->assertRedirect('test-node/', 'test-node'); + + // Test front page redirects. + $this->config('system.site')->set('page.front', '/node')->save(); + $this->assertRedirect('node', ''); + + // Test front page redirects with an alias. + \Drupal::service('path.alias_storage')->save('/node', '/node-alias'); + $this->assertRedirect('node-alias', ''); + + // Test post request. + $this->drupalPost('Test-node', 'application/json', array()); + // Does not do a redirect, stays in the same path. + $this->assertEqual(basename($this->getUrl()), 'Test-node'); + + // Test the access checking. + $this->config->set('access_check', TRUE)->save(); + $this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 403 Forbidden'); + + $this->config->set('access_check', FALSE)->save(); + // @todo - here it seems that the access check runs prior to our redirecting + // check why so and enable the test. + //$this->assertRedirect('admin/config/system/site-information', 'site-info'); + + // Test original query string is preserved with alias normalization. + $this->assertRedirect('Test-node?&foo&.bar=baz', 'test-node?&foo&.bar=baz'); + + // Test alias normalization with trailing ?. + $this->assertRedirect('test-node?', 'test-node'); + $this->assertRedirect('Test-node?', 'test-node'); + + // Test alias normalization still works without trailing ?. + $this->assertRedirect('test-node', NULL, 'HTTP/1.1 200 OK'); + $this->assertRedirect('Test-node', 'test-node'); + + // Login as user with admin privileges. + $this->drupalLogin($this->adminUser); + + // Test ignoring admin paths. + $this->config->set('ignore_admin_path', FALSE)->save(); + $this->assertRedirect('admin/config/system/site-information', 'site-info'); + + // Test alias normalization again with ignore_admin_path false. + $this->assertRedirect('Test-node', 'test-node'); + + $this->config->set('ignore_admin_path', TRUE)->save(); + $this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 200 OK'); + + // Test alias normalization again with ignore_admin_path true. + $this->assertRedirect('Test-node', 'test-node'); + } + + /** + * Test that redirects work properly with content_translation enabled. + */ + public function testLanguageRedirects() { + $this->drupalLogin($this->adminUser); + + // Add a new language. + ConfigurableLanguage::createFromLangcode('es') + ->save(); + + // Enable URL language detection and selection. + $edit = ['language_interface[enabled][language-url]' => '1']; + $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); + + // Set page content type to use multilingual support. + $edit = [ + 'language_configuration[language_alterable]' => TRUE, + 'language_configuration[content_translation]' => TRUE, + ]; + $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Page')), 'Basic page content type has been updated.'); + + $spanish_node = $this->drupalCreateNode([ + 'type' => 'page', + 'title' => 'Spanish Test Page Node', + 'path' => ['alias' => '/spanish-test-node'], + 'langcode' => 'es', + ]); + + // Test multilingual redirect. + $this->assertRedirect('es/node/' . $spanish_node->id(), 'es/spanish-test-node'); + } + + /** + * Asserts the redirect from $path to the $expected_ending_url. + * + * @param string $path + * The request path. + * @param $expected_ending_url + * The path where we expect it to redirect. If NULL value provided, no + * redirect is expected. + * @param string $expected_ending_status + * The status we expect to get with the first request. + */ + public function assertRedirect($path, $expected_ending_url, $expected_ending_status = 'HTTP/1.1 301 Moved Permanently') { + $this->drupalHead($GLOBALS['base_url'] . '/' . $path); + $headers = $this->drupalGetHeaders(TRUE); + + $ending_url = isset($headers[0]['location']) ? $headers[0]['location'] : NULL; + $message = SafeMarkup::format('Testing redirect from %from to %to. Ending url: %url', array( + '%from' => $path, + '%to' => $expected_ending_url, + '%url' => $ending_url, + )); + + + if ($expected_ending_url == '') { + $expected_ending_url = $GLOBALS['base_url'] . '/'; + } + elseif (!empty($expected_ending_url)) { + $expected_ending_url = $GLOBALS['base_url'] . '/' . $expected_ending_url; + } + else { + $expected_ending_url = NULL; + } + + $this->assertEqual($expected_ending_url, $ending_url); + + $this->assertEqual($headers[0][':status'], $expected_ending_status); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUILanguageTest.php b/sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUILanguageTest.php new file mode 100644 index 000000000..6df6e1200 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUILanguageTest.php @@ -0,0 +1,94 @@ +save(); + $language = ConfigurableLanguage::createFromLangcode('es'); + $language->save(); + } + + /** + * Test multilingual scenarios. + */ + public function testLanguageSpecificRedirects() { + $this->drupalLogin($this->adminUser); + + $this->drupalGet('admin/config/search/redirect/add'); + $this->assertOption('edit-language-0-value', 'en'); + $this->assertOption('edit-language-0-value', 'de'); + $this->assertOption('edit-language-0-value', 'es'); + $this->assertOption('edit-language-0-value', 'und'); + $this->assertNoOption('edit-language-0-value', 'zxx'); + $this->assertOptionByText('edit-language-0-value', 'English'); + $this->assertOptionByText('edit-language-0-value', 'German'); + $this->assertOptionByText('edit-language-0-value', 'Spanish'); + $this->assertOptionByText('edit-language-0-value', '- All languages -'); + + // Add a redirect for english. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'langpath', + 'redirect_redirect[0][uri]' => '/user', + 'language[0][value]' => 'en', + ), t('Save')); + + // Add a redirect for germany. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'langpath', + 'redirect_redirect[0][uri]' => '', + 'language[0][value]' => 'de', + ), t('Save')); + + // Check redirect for english. + $this->assertRedirect('langpath', '/user', 'HTTP/1.1 301 Moved Permanently'); + + // Check redirect for germany. + $this->assertRedirect('de/langpath', '/de', 'HTTP/1.1 301 Moved Permanently'); + + // Check no redirect for spanish. + $this->assertRedirect('es/langpath', NULL, 'HTTP/1.1 404 Not Found'); + } + + /** + * Test non-language specific redirect. + */ + public function testUndefinedLangugageRedirects() { + $this->drupalLogin($this->adminUser); + + // Add a redirect for english. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'langpath', + 'redirect_redirect[0][uri]' => '/user', + 'language[0][value]' => 'und', + ), t('Save')); + + // Check redirect for english. + $this->assertRedirect('langpath', '/user', 'HTTP/1.1 301 Moved Permanently'); + + // Check redirect for spanish. + $this->assertRedirect('es/langpath', '/es/user', 'HTTP/1.1 301 Moved Permanently'); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUITest.php b/sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUITest.php new file mode 100644 index 000000000..8031afc87 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/src/Tests/RedirectUITest.php @@ -0,0 +1,437 @@ +drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + $this->adminUser = $this->drupalCreateUser(array( + 'administer redirects', + 'administer redirect settings', + 'access content', + 'bypass node access', + 'create url aliases', + 'administer taxonomy', + 'administer url aliases', + )); + + $this->repository = \Drupal::service('redirect.repository'); + + $this->storage = $this->container->get('entity.manager')->getStorage('redirect'); + } + + /** + * Test the redirect UI. + */ + public function testRedirectUI() { + $this->drupalLogin($this->adminUser); + + // Test populating the redirect form with predefined values. + $this->drupalGet('admin/config/search/redirect/add', array('query' => array( + 'source' => 'non-existing', + 'source_query' => array('key' => 'val', 'key1' => 'val1'), + 'redirect' => 'node', + 'redirect_options' => array('query' => array('key' => 'val', 'key1' => 'val1')), + ))); + $this->assertFieldByName('redirect_source[0][path]', 'non-existing?key=val&key1=val1'); + $this->assertFieldByName('redirect_redirect[0][uri]', '/node?key=val&key1=val1'); + + // Test creating a new redirect via UI. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'non-existing', + 'redirect_redirect[0][uri]' => '/node', + ), t('Save')); + + // Try to find the redirect we just created. + $redirect = $this->repository->findMatchingRedirect('non-existing'); + $this->assertEqual($redirect->getSourceUrl(), Url::fromUri('base:non-existing')->toString()); + $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node')->toString()); + + // After adding the redirect we should end up in the list. Check if the + // redirect is listed. + $this->assertUrl('admin/config/search/redirect'); + $this->assertText('non-existing'); + $this->assertLink(Url::fromUri('base:node')->toString()); + $this->assertText(t('Not specified')); + + // Test the edit form and update action. + $this->clickLink(t('Edit')); + $this->assertFieldByName('redirect_source[0][path]', 'non-existing'); + $this->assertFieldByName('redirect_redirect[0][uri]', '/node'); + $this->assertFieldByName('status_code', $redirect->getStatusCode()); + + // Append a query string to see if we handle query data properly. + $this->drupalPostForm(NULL, array( + 'redirect_source[0][path]' => 'non-existing?key=value', + ), t('Save')); + + // Check the location after update and check if the value has been updated + // in the list. + $this->assertUrl('admin/config/search/redirect'); + $this->assertText('non-existing?key=value'); + + // The path field should not contain the query string and therefore we + // should be able to load the redirect using only the url part without + // query. + $this->storage->resetCache(); + $redirects = $this->repository->findBySourcePath('non-existing'); + $redirect = array_shift($redirects); + $this->assertEqual($redirect->getSourceUrl(), Url::fromUri('base:non-existing', ['query' => ['key' => 'value']])->toString()); + + // Test the source url hints. + // The hint about an existing base path. + $this->drupalPostAjaxForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'non-existing?key=value', + ), 'redirect_source[0][path]'); + $this->assertRaw(t('The base source path %source is already being redirected. Do you want to edit the existing redirect?', + array('%source' => 'non-existing?key=value', '@edit-page' => $redirect->url('edit-form')))); + + // The hint about a valid path. + $this->drupalPostAjaxForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'node', + ), 'redirect_source[0][path]'); + $this->assertRaw(t('The source path %path is likely a valid path. It is preferred to create URL aliases for existing paths rather than redirects.', + array('%path' => 'node', '@url-alias' => Url::fromRoute('path.admin_add')->toString()))); + + // Test validation. + // Duplicate redirect. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'non-existing?key=value', + 'redirect_redirect[0][uri]' => '/node', + ), t('Save')); + $this->assertRaw(t('The source path %source is already being redirected. Do you want to edit the existing redirect?', + array('%source' => 'non-existing?key=value', '@edit-page' => $redirect->url('edit-form')))); + + // Redirecting to itself. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'node', + 'redirect_redirect[0][uri]' => '/node', + ), t('Save')); + $this->assertRaw(t('You are attempting to redirect the page to itself. This will result in an infinite loop.')); + + // Redirecting the front page. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => '', + 'redirect_redirect[0][uri]' => '/node', + ), t('Save')); + $this->assertRaw(t('It is not allowed to create a redirect from the front page.')); + + // Redirecting a url with fragment. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'page-to-redirect#content', + 'redirect_redirect[0][uri]' => '/node', + ), t('Save')); + $this->assertRaw(t('The anchor fragments are not allowed.')); + + // Adding path that starts with / + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => '/page-to-redirect', + 'redirect_redirect[0][uri]' => '/node', + ), t('Save')); + $this->assertRaw(t('The url to redirect from should not start with a forward slash (/).')); + + // Test filters. + // Add a new redirect. + $this->drupalPostForm('admin/config/search/redirect/add', array( + 'redirect_source[0][path]' => 'test27', + 'redirect_redirect[0][uri]' => '/node', + ), t('Save')); + + // Filter with non existing value. + $this->drupalGet('admin/config/search/redirect', array( + 'query' => array( + 'status_code' => '3', + ), + )); + + $rows = $this->xpath('//tbody/tr'); + // Check if the list has no rows. + $this->assertTrue(count($rows) == 0); + + // Filter with existing values. + $this->drupalGet('admin/config/search/redirect', array( + 'query' => array( + 'redirect_source__path' => 'test', + 'status_code' => '2', + ), + )); + + $rows = $this->xpath('//tbody/tr'); + // Check if the list has 1 row. + $this->assertTrue(count($rows) == 1); + + $this->drupalGet('admin/config/search/redirect', array( + 'query' => array( + 'redirect_redirect__uri' => 'nod', + ), + )); + + $rows = $this->xpath('//tbody/tr'); + // Check if the list has 2 rows. + $this->assertTrue(count($rows) == 2); + + // Test the plural form of the bulk delete action. + $this->drupalGet('admin/config/search/redirect'); + $edit = [ + 'redirect_bulk_form[0]' => TRUE, + 'redirect_bulk_form[1]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, t('Apply to selected items')); + $this->assertText('Are you sure you want to delete these redirects?'); + $this->clickLink('Cancel'); + + // Test the delete action. + $this->clickLink(t('Delete')); + $this->assertRaw(t('Are you sure you want to delete the URL redirect from %source to %redirect?', + array('%source' => Url::fromUri('base:non-existing', ['query' => ['key' => 'value']])->toString(), '%redirect' => Url::fromUri('base:node')->toString()))); + $this->drupalPostForm(NULL, array(), t('Delete')); + $this->assertUrl('admin/config/search/redirect'); + + // Test the bulk delete action. + $this->drupalPostForm(NULL, ['redirect_bulk_form[0]' => TRUE], t('Apply to selected items')); + $this->assertText('Are you sure you want to delete this redirect?'); + $this->assertText('test27'); + $this->drupalPostForm(NULL, [], t('Delete')); + + $this->assertText(t('There is no redirect yet.')); + } + + /** + * Tests redirects being automatically created upon path alias change. + */ + public function testAutomaticRedirects() { + $this->drupalLogin($this->adminUser); + + // Create a node and update its path alias which should result in a redirect + // being automatically created from the old alias to the new one. + $node = $this->drupalCreateNode(array( + 'type' => 'article', + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + 'path' => array('alias' => '/node_test_alias'), + )); + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertText(t('No URL redirects available.')); + $this->drupalPostForm('node/' . $node->id() . '/edit', array('path[0][alias]' => '/node_test_alias_updated'), t('Save')); + + $redirect = $this->repository->findMatchingRedirect('node_test_alias', array(), Language::LANGCODE_NOT_SPECIFIED); + $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node_test_alias_updated')->toString()); + // Test if the automatically created redirect works. + $this->assertRedirect('node_test_alias', 'node_test_alias_updated'); + + // Test that changing the path back deletes the first redirect, creates + // a new one and does not result in a loop. + $this->drupalPostForm('node/' . $node->id() . '/edit', array('path[0][alias]' => '/node_test_alias'), t('Save')); + $redirect = $this->repository->findMatchingRedirect('node_test_alias', array(), Language::LANGCODE_NOT_SPECIFIED); + $this->assertTrue(empty($redirect)); + + \Drupal::service('path.alias_manager')->cacheClear(); + $redirect = $this->repository->findMatchingRedirect('node_test_alias_updated', array(), Language::LANGCODE_NOT_SPECIFIED); + + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertText($redirect->getSourcePathWithQuery()); + $this->assertLinkByHref(Url::fromRoute('entity.redirect.edit_form', ['redirect' => $redirect->id()])->toString()); + $this->assertLinkByHref(Url::fromRoute('entity.redirect.delete_form', ['redirect' => $redirect->id()])->toString()); + + $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node_test_alias')->toString()); + // Test if the automatically created redirect works. + $this->assertRedirect('node_test_alias_updated', 'node_test_alias'); + + // Test that the redirect will be deleted upon node deletion. + $this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete')); + $redirect = $this->repository->findMatchingRedirect('node_test_alias_updated', array(), Language::LANGCODE_NOT_SPECIFIED); + $this->assertTrue(empty($redirect)); + + // Create a term and update its path alias and check if we have a redirect + // from the previous path alias to the new one. + $term = $this->createTerm($this->createVocabulary()); + $this->drupalPostForm('taxonomy/term/' . $term->id() . '/edit', array('path[0][alias]' => '/term_test_alias_updated'), t('Save')); + $redirect = $this->repository->findMatchingRedirect('term_test_alias'); + $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:term_test_alias_updated')->toString()); + // Test if the automatically created redirect works. + $this->assertRedirect('term_test_alias', 'term_test_alias_updated'); + + // Test the path alias update via the admin path form. + $this->drupalPostForm('admin/config/search/path/add', array( + 'source' => '/node', + 'alias' => '/aaa_path_alias', + ), t('Save')); + // Note that here we rely on fact that we land on the path alias list page + // and the default sort is by the alias, which implies that the first edit + // link leads to the edit page of the aaa_path_alias. + $this->clickLink(t('Edit')); + $this->drupalPostForm(NULL, array('alias' => '/aaa_path_alias_updated'), t('Save')); + $redirect = $this->repository->findMatchingRedirect('aaa_path_alias', array(), 'en'); + $this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:aaa_path_alias_updated')->toString()); + // Test if the automatically created redirect works. + $this->assertRedirect('aaa_path_alias', 'aaa_path_alias_updated'); + + // Test the automatically created redirect shows up in the form correctly. + $this->drupalGet('admin/config/search/redirect/edit/' . $redirect->id()); + $this->assertFieldByName('redirect_source[0][path]', 'aaa_path_alias'); + $this->assertFieldByName('redirect_redirect[0][uri]', '/node'); + } + + /** + * Test the redirect loop protection and logging. + */ + function testRedirectLoop() { + // Redirect loop redirection only works when page caching is disabled. + \Drupal::service('module_installer')->uninstall(['page_cache']); + + /** @var \Drupal\redirect\Entity\Redirect $redirect1 */ + $redirect1 = $this->storage->create(); + $redirect1->setSource('node'); + $redirect1->setRedirect('admin'); + $redirect1->setStatusCode(301); + $redirect1->save(); + + /** @var \Drupal\redirect\Entity\Redirect $redirect2 */ + $redirect2 = $this->storage->create(); + $redirect2->setSource('admin'); + $redirect2->setRedirect('node'); + $redirect2->setStatusCode(301); + $redirect2->save(); + + $this->maximumRedirects = 10; + $this->drupalGet('node'); + $this->assertText('Service unavailable'); + $this->assertResponse(503); + + $log = db_select('watchdog')->fields('watchdog')->condition('type', 'redirect')->execute()->fetchAll(); + if (count($log) == 0) { + $this->fail('Redirect loop has not been logged'); + } + else { + $log = reset($log); + $this->assertEqual($log->severity, RfcLogLevel::WARNING); + $this->assertEqual(SafeMarkup::format($log->message, unserialize($log->variables)), + SafeMarkup::format('Redirect loop identified at %path for redirect %id', array('%path' => '/node', '%id' => $redirect1->id()))); + } + } + + /** + * Returns a new vocabulary with random properties. + */ + function createVocabulary() { + // Create a vocabulary. + $vocabulary = entity_create('taxonomy_vocabulary', array( + 'name' => $this->randomMachineName(), + 'description' => $this->randomMachineName(), + 'vid' => Unicode::strtolower($this->randomMachineName()), + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + 'weight' => mt_rand(0, 10), + )); + $vocabulary->save(); + return $vocabulary; + } + + /** + * Returns a new term with random properties in vocabulary $vid. + */ + function createTerm($vocabulary) { + $filter_formats = filter_formats(); + $format = array_pop($filter_formats); + $term = entity_create('taxonomy_term', array( + 'name' => $this->randomMachineName(), + 'description' => array( + 'value' => $this->randomMachineName(), + // Use the first available text format. + 'format' => $format->id(), + ), + 'vid' => $vocabulary->id(), + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + 'path' => array('alias' => '/term_test_alias'), + )); + $term->save(); + return $term; + } + + /** + * Test cache tags. + * + * @todo Not sure this belongs in a UI test, but a full web test is needed. + */ + public function testCacheTags() { + /** @var \Drupal\redirect\Entity\Redirect $redirect1 */ + $redirect1 = $this->storage->create(); + $redirect1->setSource('test-redirect'); + $redirect1->setRedirect('node'); + $redirect1->setStatusCode(301); + $redirect1->save(); + + $this->assertRedirect('test-redirect', 'node'); + $headers = $this->drupalGetHeaders(TRUE); + // Note, self::assertCacheTag() cannot be used here since it only looks at + // the final set of headers. + $expected = 'http_response ' . implode(' ', $redirect1->getCacheTags()); + $this->assertEqual($expected, $headers[0]['x-drupal-cache-tags'], 'Redirect cache tags properly set.'); + + // First request should be a cache MISS. + $this->assertEqual($headers[0]['x-drupal-cache'], 'MISS', 'First request to the redirect was not cached.'); + + // Second request should be cached. + $this->assertRedirect('test-redirect', 'node'); + $headers = $this->drupalGetHeaders(TRUE); + $this->assertEqual($headers[0]['x-drupal-cache'], 'HIT', 'The second request to the redirect was cached.'); + + // Ensure that the redirect has been cleared from cache when deleted. + $redirect1->delete(); + $this->drupalGet('test-redirect'); + $this->assertResponse(404, 'Deleted redirect properly clears the internal page cache.'); + } + + /** + * Test external destinations. + */ + public function testExternal() { + $redirect = $this->storage->create(); + $redirect->setSource('a-path'); + // @todo Redirect::setRedirect() assumes that all redirects are internal. + $redirect->redirect_redirect->set(0, ['uri' => 'https://www.example.org']); + $redirect->setStatusCode(301); + $redirect->save(); + $this->assertRedirect('a-path', 'https://www.example.org'); + $this->drupalLogin($this->adminUser); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal6.php b/sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal6.php new file mode 100644 index 000000000..fa474aae9 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal6.php @@ -0,0 +1,187 @@ +schema()->createTable('path_redirect', array( + 'fields' => array( + 'rid' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'size' => 'normal', + ), + 'source' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'redirect' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'query' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + 'fragment' => array( + 'type' => 'varchar', + 'length' => 50, + 'not null' => FALSE, + ), + 'language' => array( + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'type' => array( + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + ), + 'last_used' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('rid'), + 'unique keys' => array('source_language' => array('source', 'language')), + 'mysql_character_set' => 'utf8', +)); + + +$connection->insert('path_redirect') + ->fields(array( + 'rid', + 'source', + 'redirect', + 'query', + 'fragment', + 'language', + 'type', + 'last_used', + )) + ->values(array( + 'rid' => 5, + 'source' => 'test/source/url', + 'redirect' => 'test/redirect/url', + 'query' => NULL, + 'fragment' => NULL, + 'language' => '', + 'type' => 301, + 'last_used' => 1449497138, + )) + ->values(array( + 'rid' => 7, + 'source' => 'test/source/url2', + 'redirect' => 'http://test/external/redirect/url', + 'query' => 'foo=bar&biz=buz', + 'fragment' => NULL, + 'language' => 'en', + 'type' => 302, + 'last_used' => 1449497139, + )) + ->execute(); + +$connection->schema()->createTable('system', array( + 'fields' => array( + 'filename' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => '', + ), + 'name' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => '', + ), + 'type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => '', + ), + 'owner' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => '', + ), + 'status' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'throttle' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'bootstrap' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'schema_version' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '-1', + ), + 'weight' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'info' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'normal', + ), + ), + 'primary key' => array( + 'filename', + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('system') + ->fields(array( + 'filename', + 'name', + 'type', + 'owner', + 'status', + 'bootstrap', + 'schema_version', + 'weight', + 'info', + )) + ->values(array( + 'filename' => 'modules/contrib/redirect/redirect.module', + 'name' => 'redirect', + 'type' => 'module', + 'owner' => '', + 'status' => '1', + 'bootstrap' => '0', + 'schema_version' => '7000', + 'weight' => '0', + 'info' => 'a:10:{s:4:"name";s:8:"Redirect";s:11:"description";s:51:"Allows users to redirect from old URLs to new URLs.";s:7:"package";s:5:"Other";s:7:"version";s:3:"6.0";s:4:"core";s:3:"6.x";s:7:"project";s:8:"redirect";s:9:"datestamp";s:10:"1347989995";s:12:"dependencies";a:0:{}s:10:"dependents";a:0:{}s:3:"php";s:5:"4.3.5";}', + )) + ->execute(); + diff --git a/sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal7.php b/sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal7.php new file mode 100644 index 000000000..205107617 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/fixtures/drupal7.php @@ -0,0 +1,240 @@ +schema()->createTable('redirect', array( + 'fields' => array( + 'rid' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'size' => 'normal', + ), + 'hash' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + ), + 'uid' => array( + 'type' => 'int', + 'not null' => TRUE, + ), + 'source' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'source_options' => array( + 'type' => 'text', + 'not null' => TRUE, + ), + 'redirect' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'redirect_options' => array( + 'type' => 'text', + 'not null' => TRUE, + ), + 'language' => array( + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'status_code' => array( + 'type' => 'int', + 'size' => 'small', + 'not null' => TRUE, + ), + 'count' => array( + 'type' => 'int', + 'not null' => TRUE, + ), + 'access' => array( + 'type' => 'int', + 'not null' => TRUE, + ), + ), + 'primary key' => array('rid'), + 'unique keys' => array( + 'source_language' => array('source', 'language'), + 'expires' => array('type', 'access') + ), + 'mysql_character_set' => 'utf8', +)); + + +$connection->insert('redirect') + ->fields(array( + 'rid', + 'hash', + 'type', + 'uid', + 'source', + 'source_options', + 'redirect', + 'redirect_options', + 'language', + 'status_code', + 'count', + 'access', + )) + ->values(array( + 'rid' => 5, + 'hash' => 'MwmDbnA65ag646gtEdLqmAqTbF0qQerse63RkQmJK_Y', + 'type' => 'redirect', + 'uid' => 5, + 'source' => 'test/source/url', + 'source_options' => '', + 'redirect' => 'test/redirect/url', + 'redirect_options' => '', + 'language' => 'und', + 'status_code' => 301, + 'count' => 2518, + 'access' => 1449497138, + )) + ->values(array( + 'rid' => 7, + 'hash' => 'GvD5bBB71W8qBvp9I9hHmbSoqZfTvUz0mIkEWjlP8M4', + 'type' => 'redirect', + 'uid' => 6, + 'source' => 'test/source/url2', + 'source_options' => '', + 'redirect' => 'http://test/external/redirect/url', + 'redirect_options' => 'a:2:{s:5:"query";a:2:{s:3:"foo";s:3:"bar";s:3:"biz";s:3:"buz";}s:8:"fragment";s:10:"fragment-1";}', + 'language' => 'und', + 'status_code' => 0, + 'count' => 419, + 'access' => 1449497139, + )) + ->execute(); + +$connection->schema()->createTable('variable', array( + 'fields' => array( + 'name' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ), + 'value' => array( + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'normal', + ), + ), + 'primary key' => array( + 'name', + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('variable') +->fields(array( + 'name', + 'value', +)) +->values(array( + 'name' => 'redirect_default_status_code', + 'value' => 's:3:"307";', +)) +->execute(); + +$connection->schema()->createTable('system', array( + 'fields' => array( + 'filename' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => '', + ), + 'name' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => '', + ), + 'type' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '12', + 'default' => '', + ), + 'owner' => array( + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => '255', + 'default' => '', + ), + 'status' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'bootstrap' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'schema_version' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '-1', + ), + 'weight' => array( + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'default' => '0', + ), + 'info' => array( + 'type' => 'blob', + 'not null' => FALSE, + 'size' => 'normal', + ), + ), + 'primary key' => array( + 'filename', + ), + 'mysql_character_set' => 'utf8', +)); + +$connection->insert('system') +->fields(array( + 'filename', + 'name', + 'type', + 'owner', + 'status', + 'bootstrap', + 'schema_version', + 'weight', + 'info', +)) +->values(array( + 'filename' => 'modules/contrib/redirect/redirect.module', + 'name' => 'redirect', + 'type' => 'module', + 'owner' => '', + 'status' => '1', + 'bootstrap' => '0', + 'schema_version' => '7000', + 'weight' => '0', + 'info' => 'a:13:{s:4:"name";s:8:"Redirect";s:11:"description";s:51:"Allows users to redirect from old URLs to new URLs.";s:4:"core";s:3:"7.x";s:5:"files";a:11:{i:0;s:15:"redirect.module";i:1;s:18:"redirect.admin.inc";i:2;s:16:"redirect.install";i:3;s:13:"redirect.test";i:4;s:24:"views/redirect.views.inc";i:5;s:47:"views/redirect_handler_filter_redirect_type.inc";i:6;s:48:"views/redirect_handler_field_redirect_source.inc";i:7;s:50:"views/redirect_handler_field_redirect_redirect.inc";i:8;s:52:"views/redirect_handler_field_redirect_operations.inc";i:9;s:51:"views/redirect_handler_field_redirect_link_edit.inc";i:10;s:53:"views/redirect_handler_field_redirect_link_delete.inc";}s:9:"configure";s:37:"admin/config/search/redirect/settings";s:7:"version";s:11:"7.x-1.0-rc1";s:7:"project";s:8:"redirect";s:9:"datestamp";s:10:"1347989995";s:5:"mtime";i:1347989995;s:12:"dependencies";a:0:{}s:7:"package";s:5:"Other";s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}', +)) +->execute(); diff --git a/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d6/PathRedirectTest.php b/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d6/PathRedirectTest.php new file mode 100644 index 000000000..9383adcd1 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d6/PathRedirectTest.php @@ -0,0 +1,50 @@ +installSchema('system', array('router')); + $this->installEntitySchema('redirect'); + $this->loadFixture( __DIR__ . '/../../../../../tests/fixtures/drupal6.php'); + + $this->executeMigrations(['d6_path_redirect']); + } + + /** + * Tests the Drupal 6 path redirect to Drupal 8 migration. + */ + public function testPathRedirect() { + + /** @var Redirect $redirect */ + $redirect = Redirect::load(5); + $this->assertSame($this->getMigration('d6_path_redirect') + ->getIdMap() + ->lookupDestinationID(array(5)), array($redirect->id())); + $this->assertSame("/test/source/url", $redirect->getSourceUrl()); + $this->assertSame("base:test/redirect/url", $redirect->getRedirectUrl()->toUriString()); + + $redirect = Redirect::load(7); + $this->assertSame("/test/source/url2", $redirect->getSourceUrl()); + $this->assertSame("http://test/external/redirect/url?foo=bar&biz=buz", $redirect->getRedirectUrl()->toUriString()); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d7/PathRedirectTest.php b/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d7/PathRedirectTest.php new file mode 100644 index 000000000..469e5ccd5 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/Migrate/d7/PathRedirectTest.php @@ -0,0 +1,68 @@ +installEntitySchema('redirect'); + $this->loadFixture(__DIR__ . '/../../../../fixtures/drupal7.php'); + + $this->executeMigration('d7_path_redirect'); + } + + /** + * Asserts various aspects of a redirect entity. + * + * @param int $id + * The entity ID in the form ENTITY_TYPE.BUNDLE.FIELD_NAME. + * @param string $source_url + * The expected source url. + * @param string $redirect_url + * The expected redirect url. + * @param string $status_code + * The expected status code. + */ + protected function assertEntity($id, $source_url, $redirect_url, $status_code) { + /** @var Redirect $redirect */ + $redirect = Redirect::load($id); + $this->assertSame($this->getMigration('d7_path_redirect') + ->getIdMap() + ->lookupDestinationID([$id]), [$redirect->id()]); + $this->assertSame($source_url, $redirect->getSourceUrl()); + $this->assertSame($redirect_url, $redirect->getRedirectUrl() + ->toUriString()); + $this->assertSame($status_code, $redirect->getStatusCode()); + } + + /** + * Tests the Drupal 7 path redirect to Drupal 8 migration. + */ + public function testPathRedirect() { + $this->assertEntity(5, '/test/source/url', 'base:test/redirect/url', '301'); + $this->assertEntity(7, '/test/source/url2', 'http://test/external/redirect/url?foo=bar&biz=buz#fragment-1', '307'); + } +} diff --git a/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/RedirectAPITest.php b/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/RedirectAPITest.php new file mode 100644 index 000000000..f57e66bc0 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/src/Kernel/RedirectAPITest.php @@ -0,0 +1,292 @@ +installEntitySchema('redirect'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['router']); + $this->installConfig(array('redirect')); + + $language = ConfigurableLanguage::createFromLangcode('de'); + $language->save(); + + $this->controller = $this->container->get('entity.manager')->getStorage('redirect'); + } + + /** + * Test redirect entity logic. + */ + public function testRedirectEntity() { + // Create a redirect and test if hash has been generated correctly. + /** @var \Drupal\redirect\Entity\Redirect $redirect */ + $redirect = $this->controller->create(); + $redirect->setSource('some-url', array('key' => 'val')); + $redirect->setRedirect('node'); + + $redirect->save(); + $this->assertEquals(Redirect::generateHash('some-url', array('key' => 'val'), Language::LANGCODE_NOT_SPECIFIED), $redirect->getHash()); + // Update the redirect source query and check if hash has been updated as + // expected. + $redirect->setSource('some-url', array('key1' => 'val1')); + $redirect->save(); + $this->assertEqual(Redirect::generateHash('some-url', array('key1' => 'val1'), Language::LANGCODE_NOT_SPECIFIED), $redirect->getHash()); + // Update the redirect source path and check if hash has been updated as + // expected. + $redirect->setSource('another-url', array('key1' => 'val1')); + $redirect->save(); + $this->assertEqual(Redirect::generateHash('another-url', array('key1' => 'val1'), Language::LANGCODE_NOT_SPECIFIED), $redirect->getHash()); + // Update the redirect language and check if hash has been updated as + // expected. + $redirect->setLanguage('de'); + $redirect->save(); + $this->assertEqual(Redirect::generateHash('another-url', array('key1' => 'val1'), 'de'), $redirect->getHash()); + // Create a few more redirects to test the select. + for ($i = 0; $i < 5; $i++) { + $redirect = $this->controller->create(); + $redirect->setSource($this->randomMachineName()); + $redirect->save(); + } + /** @var \Drupal\redirect\RedirectRepository $repository */ + $repository = \Drupal::service('redirect.repository'); + $redirect = $repository->findMatchingRedirect('another-url', array('key1' => 'val1'), 'de'); + if (!empty($redirect)) { + $this->assertEqual($redirect->getSourceUrl(), '/another-url?key1=val1'); + } + else { + $this->fail(t('Failed to find matching redirect.')); + } + + // Load the redirect based on url. + $redirects = $repository->findBySourcePath('another-url'); + $redirect = array_shift($redirects); + if (!empty($redirect)) { + $this->assertEqual($redirect->getSourceUrl(), '/another-url?key1=val1'); + } + else { + $this->fail(t('Failed to find redirect by source path.')); + } + + // Test passthrough_querystring. + $redirect = $this->controller->create(); + $redirect->setSource('a-different-url'); + $redirect->setRedirect('node'); + $redirect->save(); + $redirect = $repository->findMatchingRedirect('a-different-url', ['key1' => 'val1'], 'de'); + if (!empty($redirect)) { + $this->assertEqual($redirect->getSourceUrl(), '/a-different-url'); + } + else { + $this->fail('Failed to find redirect by source path with query string.'); + } + + // Add another redirect to the same path, with a query. This should always + // be found before the source without a query set. + /** @var \Drupal\redirect\Entity\Redirect $new_redirect */ + $new_redirect = $this->controller->create(); + $new_redirect->setSource('a-different-url', ['foo' => 'bar']); + $new_redirect->setRedirect('node'); + $new_redirect->save(); + $found = $repository->findMatchingRedirect('a-different-url', ['foo' => 'bar'], 'de'); + if (!empty($found)) { + $this->assertEqual($found->getSourceUrl(), '/a-different-url?foo=bar'); + } + else { + $this->fail('Failed to find a redirect by source path with query string.'); + } + + // Hashes should be case-insensitive since the source paths are. + /** @var \Drupal\redirect\Entity\Redirect $redirect */ + $redirect = $this->controller->create(); + $redirect->setSource('Case-Sensitive-Path'); + $redirect->setRedirect('node'); + $redirect->save(); + $found = $repository->findBySourcePath('case-sensitive-path'); + if (!empty($found)) { + $found = reset($found); + $this->assertEqual($found->getSourceUrl(), '/Case-Sensitive-Path'); + } + else { + $this->fail('findBySourcePath is case sensitive'); + } + $found = $repository->findMatchingRedirect('case-sensitive-path'); + if (!empty($found)) { + $this->assertEqual($found->getSourceUrl(), '/Case-Sensitive-Path'); + } + else { + $this->fail('findMatchingRedirect is case sensitive.'); + } + } + + /** + * Test slash is removed from source path in findMatchingRedirect. + */ + public function testDuplicateRedirectEntry() { + $redirect = $this->controller->create(); + $redirect->setSource('/foo/foo', []); + $redirect->setRedirect('foo'); + $redirect->save(); + + $redirect_repository = \Drupal::service('redirect.repository'); + $matched_redirect = $redirect_repository->findMatchingRedirect('/foo/foo', [], 'en-AU'); + $this->assertNotNull($matched_redirect); + + $null_redirect = $redirect_repository->findMatchingRedirect('/foo/foo-bar', [], 'en-AU'); + $this->assertNull($null_redirect); + } + + /** + * Test redirect_sort_recursive(). + */ + public function testSortRecursive() { + $test_cases = array( + array( + 'input' => array('b' => 'aa', 'c' => array('c2' => 'aa', 'c1' => 'aa'), 'a' => 'aa'), + 'expected' => array('a' => 'aa', 'b' => 'aa', 'c' => array('c1' => 'aa', 'c2' => 'aa')), + 'callback' => 'ksort', + ), + ); + foreach ($test_cases as $index => $test_case) { + $output = $test_case['input']; + redirect_sort_recursive($output, $test_case['callback']); + $this->assertIdentical($output, $test_case['expected']); + } + } + + /** + * Test loop detection. + */ + public function testLoopDetection() { + // Add a chained redirect that isn't a loop. + /** @var \Drupal\redirect\Entity\Redirect $one */ + $one = $this->controller->create(); + $one->setSource('my-path'); + $one->setRedirect('node'); + $one->save(); + /** @var \Drupal\redirect\Entity\Redirect $two */ + $two = $this->controller->create(); + $two->setSource('second-path'); + $two->setRedirect('my-path'); + $two->save(); + /** @var \Drupal\redirect\Entity\Redirect $three */ + $three = $this->controller->create(); + $three->setSource('third-path'); + $three->setRedirect('second-path'); + $three->save(); + + /** @var \Drupal\redirect\RedirectRepository $repository */ + $repository = \Drupal::service('redirect.repository'); + $found = $repository->findMatchingRedirect('third-path'); + if (!empty($found)) { + $this->assertEqual($found->getRedirectUrl()->toString(), '/node', 'Chained redirects properly resolved in findMatchingRedirect.'); + } + else { + $this->fail('Failed to resolve a chained redirect.'); + } + + // Create a loop. + $one->setRedirect('third-path'); + $one->save(); + try { + $repository->findMatchingRedirect('third-path'); + $this->fail('Failed to detect a redirect loop.'); + } + catch (RedirectLoopException $e) { + $this->pass('Properly detected a redirect loop.'); + } + } + + /** + * Test redirect_parse_url(). + */ + public function testParseURL() { + //$test_cases = array( + // array( + // 'input' => array('b' => 'aa', 'c' => array('c2' => 'aa', 'c1' => 'aa'), 'a' => 'aa'), + // 'expected' => array('a' => 'aa', 'b' => 'aa', 'c' => array('c1' => 'aa', 'c2' => 'aa')), + // ), + //); + //foreach ($test_cases as $index => $test_case) { + // $output = redirect_parse_url($test_case['input']); + // $this->assertIdentical($output, $test_case['expected']); + //} + } + + /** + * Test multilingual redirects. + */ + public function testMultilanguageCases() { + // Add a redirect for english. + /** @var \Drupal\redirect\Entity\Redirect $en_redirect */ + $en_redirect = $this->controller->create(); + $en_redirect->setSource('langpath'); + $en_redirect->setRedirect('/about'); + $en_redirect->setLanguage('en'); + $en_redirect->save(); + + // Add a redirect for germany. + /** @var \Drupal\redirect\Entity\Redirect $en_redirect */ + $en_redirect = $this->controller->create(); + $en_redirect->setSource('langpath'); + $en_redirect->setRedirect('node'); + $en_redirect->setLanguage('de'); + $en_redirect->save(); + + // Check redirect for english. + /** @var \Drupal\redirect\RedirectRepository $repository */ + $repository = \Drupal::service('redirect.repository'); + + $found = $repository->findBySourcePath('langpath'); + if (!empty($found)) { + $this->assertEqual($found[1]->getRedirectUrl()->toString(), '/about', 'Multilingual redirect resolved properly.'); + $this->assertEqual($found[1]->get('language')[0]->value, 'en', 'Multilingual redirect resolved properly.'); + } + else { + $this->fail('Failed to resolve the multilingual redirect.'); + } + + // Check redirect for germany. + \Drupal::configFactory()->getEditable('system.site')->set('default_langcode', 'de')->save(); + /** @var \Drupal\redirect\RedirectRepository $repository */ + $repository = \Drupal::service('redirect.repository'); + $found = $repository->findBySourcePath('langpath'); + if (!empty($found)) { + $this->assertEqual($found[2]->getRedirectUrl()->toString(), '/node', 'Multilingual redirect resolved properly.'); + $this->assertEqual($found[2]->get('language')[0]->value, 'de', 'Multilingual redirect resolved properly.'); + } + else { + $this->fail('Failed to resolve the multilingual redirect.'); + } + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/tests/src/Unit/Migrate/d7/PathRedirectTest.php b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/Migrate/d7/PathRedirectTest.php new file mode 100644 index 000000000..8a5c593e6 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/Migrate/d7/PathRedirectTest.php @@ -0,0 +1,54 @@ + 'test', + 'source' => [ + 'plugin' => 'd7_path_redirect', + ], + ]; + + protected $expectedResults = [ + [ + 'rid' => 5, + 'hash' => 'MwmDbnA65ag646gtEdLqmAqTbF0qQerse63RkQmJK_Y', + 'type' => 'redirect', + 'uid' => 5, + 'source' => 'test/source/url', + 'source_options' => '', + 'redirect' => 'test/redirect/url', + 'redirect_options' => '', + 'language' => 'und', + 'status_code' => 301, + 'count' => 2518, + 'access' => 1449497138, + ], + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->databaseContents['variable'] = [ + [ + 'name' => 'redirect_default_status_code', + 'value' => 's:3:"307";', + ] + ]; + $this->databaseContents['redirect'] = $this->expectedResults; + parent::setUp(); + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectCheckerTest.php b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectCheckerTest.php new file mode 100644 index 000000000..35f823323 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectCheckerTest.php @@ -0,0 +1,155 @@ + array('ignore_admin_path' => FALSE, 'access_check' => TRUE)); + + $state = $this->getMockBuilder('Drupal\Core\State\StateInterface') + ->getMock(); + $state->expects($this->any()) + ->method('get') + ->with('system.maintenance_mode') + ->will($this->returnValue(FALSE)); + $access = $this->getMockBuilder('Drupal\Core\Access\AccessManager') + ->disableOriginalConstructor() + ->getMock(); + $account = $this->getMockBuilder('Drupal\Core\Session\AccountInterface') + ->getMock(); + $route_provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProviderInterface') + ->getMock(); + + $route = new Route('/example'); + $route_provider->expects($this->any()) + ->method('getRouteByName') + ->willReturn($route); + + $access->expects($this->any()) + ->method('checkNamedRoute') + ->willReturnMap([ + ['denied_route', [], $account, FALSE, FALSE], + ['allowed_route', [], $account, FALSE, TRUE], + ]); + + $checker = new RedirectChecker($this->getConfigFactoryStub($config), $state, $access, $account, $route_provider); + + // All fine - we can redirect. + $request = $this->getRequestStub('index.php', 'GET'); + $this->assertTrue($checker->canRedirect($request), 'Can redirect'); + + // The script name is not index.php. + $request = $this->getRequestStub('statistics.php', 'GET'); + $this->assertFalse($checker->canRedirect($request), 'Cannot redirect script name not index.php'); + + // The request method is not GET. + $request = $this->getRequestStub('index.php', 'POST'); + $this->assertFalse($checker->canRedirect($request), 'Cannot redirect other than GET method'); + + + // Route access check, deny access. + $request = $this->getRequestStub('index.php', 'GET'); + $this->assertFalse($checker->canRedirect($request, 'denied_route'), 'Can not redirect'); + + // Route access check, allow access. + $request = $this->getRequestStub('index.php', 'GET'); + $this->assertTrue($checker->canRedirect($request, 'allowed_route'), 'Can redirect'); + + // Check destination parameter. + $request = $this->getRequestStub('index.php', 'GET', [], ['destination' => 'paradise']); + $this->assertFalse($checker->canRedirect($request), 'Cannot redirect'); + + // Maintenance mode is on. + $state = $this->getMockBuilder('Drupal\Core\State\StateInterface') + ->getMock(); + $state->expects($this->any()) + ->method('get') + ->with('system.maintenance_mode') + ->will($this->returnValue(TRUE)); + + $checker = new RedirectChecker($this->getConfigFactoryStub($config), $state, $access, $account, $route_provider); + + $request = $this->getRequestStub('index.php', 'GET'); + $this->assertFalse($checker->canRedirect($request), 'Cannot redirect if maintenance mode is on'); + + // We are at a admin path. + $state = $this->getMockBuilder('Drupal\Core\State\StateInterface') + ->getMock(); + $state->expects($this->any()) + ->method('get') + ->with('system.maintenance_mode') + ->will($this->returnValue(FALSE)); + +// $checker = new RedirectChecker($this->getConfigFactoryStub($config), $state); +// +// $route = $this->getMockBuilder('Symfony\Component\Routing\Route') +// ->disableOriginalConstructor() +// ->getMock(); +// $route->expects($this->any()) +// ->method('getOption') +// ->with('_admin_route') +// ->will($this->returnValue('system.admin_config_search')); +// +// $request = $this->getRequestStub('index.php', 'GET', +// array(RouteObjectInterface::ROUTE_OBJECT => $route)); +// $this->assertFalse($checker->canRedirect($request), 'Cannot redirect if we are requesting a admin path'); +// +// // We are at admin path with ignore_admin_path set to TRUE. +// $config['redirect.settings']['ignore_admin_path'] = TRUE; +// $checker = new RedirectChecker($this->getConfigFactoryStub($config), $state); +// +// $request = $this->getRequestStub('index.php', 'GET', +// array(RouteObjectInterface::ROUTE_OBJECT => $route)); +// $this->assertTrue($checker->canRedirect($request), 'Can redirect a admin with ignore_admin_path set to TRUE'); + } + + /** + * Gets request mock object. + * + * @param string $script_name + * The result of the getScriptName() method. + * @param string $method + * The request method. + * @param array $attributes + * Attributes to be passed into request->attributes. + * @param array $query + * Query paramter to be passed into request->query. + * + * @return PHPUnit_Framework_MockObject_MockObject + * Mocked request object. + */ + protected function getRequestStub($script_name, $method, array $attributes = [], array $query = []) { + $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + $request->expects($this->any()) + ->method('getScriptName') + ->will($this->returnValue($script_name)); + $request->expects($this->any()) + ->method('isMethod') + ->with($this->anything()) + ->will($this->returnValue($method == 'GET')); + $request->query = new ParameterBag($query); + $request->attributes = new ParameterBag($attributes); + + return $request; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectRequestSubscriberTest.php b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectRequestSubscriberTest.php new file mode 100644 index 000000000..4982bdb6e --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RedirectRequestSubscriberTest.php @@ -0,0 +1,282 @@ +getMockBuilder('Drupal\Core\Url') + ->disableOriginalConstructor() + ->getMock(); + + $url->expects($this->once()) + ->method('setAbsolute') + ->with(TRUE) + ->willReturn($url); + + $url->expects($this->once()) + ->method('getOption') + ->with('query') + ->willReturn($redirect_query); + + $url->expects($this->once()) + ->method('setOption') + ->with('query', $final_query); + + $url->expects($this->once()) + ->method('toString') + ->willReturn($redirect_uri); + + $redirect = $this->getRedirectStub($url); + $event = $this->callOnKernelRequestCheckRedirect($redirect, $request_uri, $request_query, TRUE); + + $this->assertTrue($event->getResponse() instanceof RedirectResponse); + $response = $event->getResponse(); + $this->assertEquals('/test-path', $response->getTargetUrl()); + $this->assertEquals(301, $response->getStatusCode()); + $this->assertEquals(1, $response->headers->get('X-Redirect-ID')); + } + + /** + * @covers ::onKernelRequestCheckRedirect + * @dataProvider getRedirectData + */ + public function testRedirectLogicWithoutQueryRetaining($request_uri, $request_query, $redirect_uri) { + + $url = $this->getMockBuilder('Drupal\Core\Url') + ->disableOriginalConstructor() + ->getMock(); + + $url->expects($this->once()) + ->method('setAbsolute') + ->with(TRUE) + ->willReturn($url); + + // No query retaining, so getOption should not be called. + $url->expects($this->never()) + ->method('getOption'); + $url->expects($this->never()) + ->method('setOption'); + + $url->expects($this->once()) + ->method('toString') + ->willReturn($redirect_uri); + + $redirect = $this->getRedirectStub($url); + $event = $this->callOnKernelRequestCheckRedirect($redirect, $request_uri, $request_query, FALSE); + + $this->assertTrue($event->getResponse() instanceof RedirectResponse); + $response = $event->getResponse(); + $this->assertEquals($redirect_uri, $response->getTargetUrl()); + $this->assertEquals(301, $response->getStatusCode()); + $this->assertEquals(1, $response->headers->get('X-Redirect-ID')); + } + + /** + * Data provider for both tests. + */ + public function getRedirectData() { + return [ + ['non-existing', ['key' => 'val'], '/test-path', ['dummy' => 'value']], + ['non-existing/', ['key' => 'val'], '/test-path', ['dummy' => 'value']], + ]; + } + + /** + * Instantiates the subscriber and runs onKernelRequestCheckRedirect() + * + * @param $redirect + * The redirect entity. + * @param $request_uri + * The URI of the request. + * @param array $request_query + * The query that is supposed to come via request. + * @param bool $retain_query + * Flag if to retain the query through the redirect. + * + * @return \Symfony\Component\HttpKernel\Event\GetResponseEvent + * THe response event. + */ + protected function callOnKernelRequestCheckRedirect($redirect, $request_uri, $request_query, $retain_query) { + + $event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query)); + $request = $event->getRequest(); + + $checker = $this->getMockBuilder('Drupal\redirect\RedirectChecker') + ->disableOriginalConstructor() + ->getMock(); + $checker->expects($this->any()) + ->method('canRedirect') + ->will($this->returnValue(TRUE)); + $checker->expects($this->any()) + ->method('isLoop') + ->will($this->returnValue(FALSE)); + + $context = $this->getMock('Symfony\Component\Routing\RequestContext'); + + $inbound_path_processor = $this->getMockBuilder('Drupal\Core\PathProcessor\InboundPathProcessorInterface') + ->disableOriginalConstructor() + ->getMock(); + $inbound_path_processor->expects($this->any()) + ->method('processInbound') + ->with($request->getPathInfo(), $request) + ->will($this->returnValue($request->getPathInfo())); + + $alias_manager = $this->getMockBuilder('Drupal\Core\Path\AliasManager') + ->disableOriginalConstructor() + ->getMock(); + $module_handler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandlerInterface') + ->getMock(); + $entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManagerInterface') + ->getMock(); + + $subscriber = new RedirectRequestSubscriber( + $this->getRedirectRepositoryStub('findMatchingRedirect', $redirect), + $this->getLanguageManagerStub(), + $this->getConfigFactoryStub(array('redirect.settings' => array('passthrough_querystring' => $retain_query))), + $alias_manager, + $module_handler, + $entity_manager, + $checker, + $context, + $inbound_path_processor + ); + + // Run the main redirect method. + $subscriber->onKernelRequestCheckRedirect($event); + return $event; + } + + /** + * Gets the redirect repository mock object. + * + * @param $method + * Method to mock - either load() or findMatchingRedirect(). + * @param $redirect + * The redirect object to be returned. + * + * @return PHPUnit_Framework_MockObject_MockObject + * The redirect repository. + */ + protected function getRedirectRepositoryStub($method, $redirect) { + $repository = $this->getMockBuilder('Drupal\redirect\RedirectRepository') + ->disableOriginalConstructor() + ->getMock(); + + $repository->expects($this->any()) + ->method($method) + ->will($this->returnValue($redirect)); + + return $repository; + } + + /** + * Gets the redirect mock object. + * + * @param $url + * Url to be returned from getRedirectUrl + * @param int $status_code + * The redirect status code. + * + * @return PHPUnit_Framework_MockObject_MockObject + * The mocked redirect object. + */ + protected function getRedirectStub($url, $status_code = 301) { + $redirect = $this->getMockBuilder('Drupal\redirect\Entity\Redirect') + ->disableOriginalConstructor() + ->getMock(); + $redirect->expects($this->once()) + ->method('getRedirectUrl') + ->will($this->returnValue($url)); + $redirect->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue($status_code)); + $redirect->expects($this->any()) + ->method('id') + ->willReturn(1); + $redirect->expects($this->once()) + ->method('getCacheTags') + ->willReturn(['redirect:1']); + + return $redirect; + } + + /** + * Gets post response event. + * + * @param array $headers + * Headers to be set into the response. + * + * @return \Symfony\Component\HttpKernel\Event\PostResponseEvent + * The post response event object. + */ + protected function getPostResponseEvent($headers = array()) { + $http_kernel = $this->getMockBuilder('\Symfony\Component\HttpKernel\HttpKernelInterface') + ->getMock(); + $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + + $response = new Response('', 301, $headers); + + return new PostResponseEvent($http_kernel, $request, $response); + } + + /** + * Gets response event object. + * + * @param $path_info + * @param $query_string + * + * @return GetResponseEvent + */ + protected function getGetResponseEventStub($path_info, $query_string) { + $request = Request::create($path_info . '?' . $query_string, 'GET', [], [], [], ['SCRIPT_NAME' => 'index.php']); + + $http_kernel = $this->getMockBuilder('\Symfony\Component\HttpKernel\HttpKernelInterface') + ->getMock(); + return new GetResponseEvent($http_kernel, $request, 'test'); + } + + /** + * Gets the language manager mock object. + * + * @return \Drupal\language\ConfigurableLanguageManagerInterface|PHPUnit_Framework_MockObject_MockObject + */ + protected function getLanguageManagerStub() { + $language_manager = $this->getMockBuilder('Drupal\language\ConfigurableLanguageManagerInterface') + ->getMock(); + $language_manager->expects($this->any()) + ->method('getCurrentLanguage') + ->will($this->returnValue(new Language(array('id' => 'en')))); + + return $language_manager; + } + +} diff --git a/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RouteNormalizerRequestSubscriberTest.php b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RouteNormalizerRequestSubscriberTest.php new file mode 100644 index 000000000..87d3c3135 --- /dev/null +++ b/sites/all/modules/contrib/admin/redirect/tests/src/Unit/RouteNormalizerRequestSubscriberTest.php @@ -0,0 +1,225 @@ +getMock('\Drupal\Core\PageCache\ResponsePolicy\KillSwitch'); + $kill_switch->expects($this->any()) + ->method('trigger') + ->withAnyParameters() + ->will($this->returnValue(NULL)); + $container = new ContainerBuilder(); + $container->set('page_cache_kill_switch', $kill_switch); + \Drupal::setContainer($container); + } + + /** + * @covers ::onKernelRequestRedirect + */ + public function testSkipIfFlagNotEnabled() { + $request_uri = 'https://example.com/route-to-normalize'; + $request_query = []; + + $event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query)); + // We set 'route_normalizer_enabled' config to FALSE and expect to leave onKernelRequestRedirect at the beginning, + // i.e. $this->redirectChecker->canRedirect($request) should never be called. + $subscriber = $this->getSubscriber($request_uri, FALSE, FALSE); + $subscriber->onKernelRequestRedirect($event); + } + + /** + * @covers ::onKernelRequestRedirect + */ + public function testSkipIfSubRequest() { + $request_uri = 'https://example.com/route-to-normalize'; + $request_query = []; + + $event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query), HttpKernelInterface::SUB_REQUEST); + // We are using SUB_REQUEST as the request type and expect to leave onKernelRequestRedirect at the beginning, + // i.e. $this->redirectChecker->canRedirect($request) should never be called. + $subscriber = $this->getSubscriber($request_uri, TRUE, FALSE); + $subscriber->onKernelRequestRedirect($event); + } + + /** + * @covers ::onKernelRequestRedirect + */ + public function testSkipIfRequestAttribute() { + $request_uri = 'https://example.com/route-to-normalize'; + $request_query = []; + + $event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query), HttpKernelInterface::MASTER_REQUEST, TRUE); + // We set '_disable_route_normalizer' as a request attribute and expect to leave onKernelRequestRedirect at the beginning, + // i.e. $this->redirectChecker->canRedirect($request) should never be called. + $subscriber = $this->getSubscriber($request_uri, TRUE, FALSE); + $subscriber->onKernelRequestRedirect($event); + } + + /** + * @covers ::onKernelRequestRedirect + * @dataProvider getTestUrls + */ + public function testOnKernelRequestRedirect($request_uri, $request_query, $expected, $expect_normalization) { + $event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query)); + $subscriber = $this->getSubscriber($request_uri); + $subscriber->onKernelRequestRedirect($event); + + if ($expect_normalization) { + $response = $event->getResponse(); + $this->assertEquals($expected, $response->getTargetUrl()); + } + } + + /** + * Data provider for testOnKernelRequestRedirect(). + */ + public function getTestUrls() { + return [ + ['https://example.com/route-to-normalize', [], 'https://example.com/route-to-normalize', FALSE], + ['https://example.com/route-to-normalize', ['key' => 'value'], 'https://example.com/route-to-normalize?key=value', FALSE], + ['https://example.com/index.php/', ['q' => 'node/1'], 'https://example.com/?q=node/1', TRUE], + ]; + } + + /** + * Create a RouteNormalizerRequestSubscriber object. + * + * @param string $request_uri + * The return value for the generateFromRoute method. + * @param bool $enabled + * Flag indicating if the normalizer shoud be enabled. + * @param bool $call_expected + * If true, canRedirect() and other methods should be called once. + * + * @return \Drupal\redirect\EventSubscriber\RouteNormalizerRequestSubscriber + */ + protected function getSubscriber($request_uri, $enabled = TRUE, $call_expected = TRUE) { + return new RouteNormalizerRequestSubscriber( + $this->getUrlGeneratorStub($request_uri, $call_expected), + $this->getPathMatcherStub($call_expected), + $this->getConfigFactoryStub([ + 'redirect.settings' => [ + 'route_normalizer_enabled' => $enabled, + 'default_status_code' => 301, + ], + ]), + $this->getRedirectCheckerStub($call_expected) + ); + } + + /** + * Gets the UrlGenerator mock object. + * + * @param string $request_uri + * The return value for the generateFromRoute method. + * @param bool $call_expected + * If true, we expect generateFromRoute() to be called once. + * + * @return \Drupal\Core\Routing\UrlGeneratorInterface|PHPUnit_Framework_MockObject_MockObject + */ + protected function getUrlGeneratorStub($request_uri, $call_expected = TRUE) { + $url_generator = $this->getMockBuilder('\Drupal\Core\Routing\UrlGeneratorInterface') + ->getMock(); + + $options = ['absolute' => TRUE]; + + $expectation = $call_expected ? $this->once() : $this->never(); + + $url_generator->expects($expectation) + ->method('generateFromRoute') + ->with('', [], $options) + ->willReturn($request_uri); + return $url_generator; + } + + /** + * Gets the PathMatcher mock object. + * + * @param bool $call_expected + * If true, we expect isFrontPage() to be called once. + * + * @return \Drupal\Core\Path\PathMatcherInterface|PHPUnit_Framework_MockObject_MockObject + */ + protected function getPathMatcherStub($call_expected = TRUE) { + $path_matcher = $this->getMockBuilder('\Drupal\Core\Path\PathMatcherInterface') + ->getMock(); + + $expectation = $call_expected ? $this->once() : $this->never(); + + $path_matcher->expects($expectation) + ->method('isFrontPage') + ->withAnyParameters() + ->willReturn(FALSE); + return $path_matcher; + } + + /** + * Gets the RedirectChecker mock object. + * + * @param bool $call_expected + * If true, we expect canRedirect() to be called once. + * + * @return \Drupal\redirect\RedirectChecker|PHPUnit_Framework_MockObject_MockObject + */ + protected function getRedirectCheckerStub($call_expected = TRUE) { + $redirect_checker = $this->getMockBuilder('\Drupal\redirect\RedirectChecker') + ->disableOriginalConstructor() + ->getMock(); + + $expectation = $call_expected ? $this->once() : $this->never(); + + $redirect_checker->expects($expectation) + ->method('canRedirect') + ->withAnyParameters() + ->willReturn(TRUE); + return $redirect_checker; + } + + /** + * Returns a GET response event object. + * + * @param string $path_info + * The path of the request. + * @param array $query_string + * The query string of the request. + * @param int $request_type + * The request type of the request. + * @param bool $set_request_attribute + * If true, the request attribute '_disable_route_normalizer' will be set. + * + * @return \Symfony\Component\HttpKernel\Event\GetResponseEvent + */ + protected function getGetResponseEventStub($path_info, $query_string, $request_type = HttpKernelInterface::MASTER_REQUEST, $set_request_attribute = FALSE) { + $request = Request::create($path_info . '?' . $query_string, 'GET', [], [], [], ['SCRIPT_NAME' => 'index.php', 'SCRIPT_FILENAME' => 'index.php']); + + if ($set_request_attribute === TRUE) { + $request->attributes->add(['_disable_route_normalizer' => TRUE]); + } + + $http_kernel = $this->getMockBuilder('\Symfony\Component\HttpKernel\HttpKernelInterface') + ->getMock(); + return new GetResponseEvent($http_kernel, $request, $request_type); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/LICENSE.txt b/sites/all/modules/contrib/views/translation_views/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/LICENSE.txt @@ -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. diff --git a/sites/all/modules/contrib/views/translation_views/README.md b/sites/all/modules/contrib/views/translation_views/README.md new file mode 100644 index 000000000..a1ed493f7 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/README.md @@ -0,0 +1,65 @@ +Translation View +============= + +## Description: +Provides fields and filter for views to display information about the current row, the **source** translation, in another **target** language. You can also add translation operation links to decide if you want to add/edit the content in the target language. + +Configurations for a demo view is impored on installation, named `Content translation jobs` which can be found at path `/translate/content`. If you want to get view for other translatable entities, you should build it yourself using our fields/filters. + +**Notice**: This module provides view's fields/filters only for translatble entity types, this means that until you have enabled content translation support for an entity you'll not get provided any extra fields/filters. The content translations can be enabled at path `admin/config/regional/content-language` + +## Features + +**Target translation** + +- Target language: `field`,`filter`. Should in most cases be an exposed filter. + Also there is an extra filter option: *Remove rows where source language is equal to target language* + +These fields/filters will display information about the current content in the selected target language: +- Translation outdated: `field`,`filter` +- Translation status: `field`,`filter` +- Target language equals default language: `field`,`filter`: Checks if the target language is the same as the original language of the node. +- Translation changed time: `field` +- Translation source equals row language: `field`,`filter`: Checks if the source translation of the target language is the same as the language of the row +- Translation operations: `field` + +**Source translation** +- Translation counter: `field`,`filter` + +## Requirements: +- Drupal 8 +- PHP 5.6+ + +## Installation: +Install module as usually. + +### Demo view: Content translation jobs: +Upon installation a new view is added named `Content translation jobs` that demonstrate how the fields/filters can be used to create lists of translation jobs. Available at path `/translate/content`. + +If this didn't happened on installation than go to the`admin/config/development/configuration/single/import` and manually import config from the file located in the module folder at: `config/optional/views.view.content_translations.yml`. + +**Fields** +The view display: +- Title for each node in the view +- The node language +- The target language +- The translation status of target language. +- The time the content was last changed in target language +- Translation operations to add/edit translation in target language + +**Filters** +Exposed filters: +- Source language (the language of each node in the view) +- Target language +- Outdated status in target language +- Translation status in target language + +Hidden filters +- Source content outdated must be false: Make sure you do not translation from outdated nodes. +- Target language equals default language must be false: As there is no use to check status of original language as a target language. +- The target language must be untranslated or outdated: Make sure translated and updated content disapear from the list. +- The target language must be untranslated or the same as the source language of translation: This makes sure that once there is a translation in target language it will only be displayed if the source language is the correct source language. If target language is not translated you can use different language as source translation. + +## Credits +Developed by [vlad.dancer](https://drupal.org/u/vladdancer) +Designed by [matsbla](https://drupal.org/u/matsbla) diff --git a/sites/all/modules/contrib/views/translation_views/config/install/views.view.content_translations.yml b/sites/all/modules/contrib/views/translation_views/config/install/views.view.content_translations.yml new file mode 100644 index 000000000..61aa26e1a --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/config/install/views.view.content_translations.yml @@ -0,0 +1,952 @@ +langcode: en +status: true +dependencies: + module: + - node + - translation_views + - user +id: content_translations +label: 'Content translation jobs' +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: 'create content translations' + 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: true + 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: 50 + offset: 0 + id: 0 + total_pages: null + tags: + previous: ‹‹ + next: ›› + first: '« First' + last: 'Last »' + 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 + quantity: 9 + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + title: title + langcode: langcode + translation_target_language: translation_target_language + translation_outdated: translation_outdated + translation_status: translation_status + changed: changed + translation_operations: translation_operations + info: + title: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + langcode: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + translation_target_language: + align: '' + separator: '' + empty_column: false + responsive: '' + translation_outdated: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + translation_status: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + translation_operations: + align: '' + separator: '' + empty_column: false + responsive: '' + default: changed + empty_table: false + 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 + langcode: + id: langcode + table: node_field_data + field: langcode + relationship: none + group_type: group + admin_label: '' + label: From + 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 + click_sort_column: value + type: language + settings: + link_to_entity: false + native_language: false + 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 + entity_type: node + entity_field: langcode + plugin_id: field + translation_target_language: + id: translation_target_language + table: node_translation + field: translation_target_language + relationship: none + group_type: group + admin_label: '' + label: To + 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 + native_language: 0 + entity_type: node + plugin_id: translation_views_target_language + translation_outdated: + id: translation_outdated + table: node_translation_target + field: translation_outdated + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: true + 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 + type: boolean + type_custom_true: '' + type_custom_false: '' + not: false + plugin_id: boolean + translation_status: + id: translation_status + table: node_translation + field: translation_status + relationship: none + group_type: group + admin_label: '' + label: 'Translation status' + exclude: false + alter: + alter_text: true + text: "{{ translation_status }}\n{% if translation_outdated %} {{ 'outdated'|t }}{% endif %}" + 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 + type: status + type_custom_true: ✅ + type_custom_false: ❌ + not: false + plugin_id: boolean + translation_changed: + id: translation_changed + table: node_translation_target + field: translation_changed + relationship: none + group_type: group + admin_label: '' + label: Changed + 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 + date_format: fallback + custom_date_format: '' + timezone: '' + entity_field: changed + plugin_id: date + translation_operations: + id: translation_operations + table: node_translation + field: translation_operations + relationship: none + group_type: group + admin_label: '' + label: 'Translation operations' + 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 + destination: true + entity_type: node + plugin_id: translation_views_operations + filters: + langcode: + id: langcode + table: node_field_data + field: langcode + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: langcode_op + label: 'Source language' + description: '' + use_operator: false + operator: langcode_op + identifier: langcode + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: langcode + plugin_id: language + content_translation_outdated: + id: content_translation_outdated + table: node_field_data + field: content_translation_outdated + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '0' + group: 1 + exposed: false + expose: + operator_id: '' + label: 'Translation outdated' + description: '' + use_operator: false + operator: content_translation_outdated_op + identifier: content_translation_outdated + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: content_translation_outdated + plugin_id: boolean + translation_target_language: + id: translation_target_language + table: node_translation + field: translation_target_language + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: { } + group: 1 + exposed: true + expose: + operator_id: '' + label: 'Target language' + description: '' + use_operator: false + operator: translation_target_language_op + identifier: translation_target_language + required: true + remember: true + multiple: true + remember_roles: + anonymous: anonymous + authenticated: authenticated + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + remove: 1 + entity_type: node + plugin_id: translation_views_target_language + translation_outdated: + id: translation_outdated + table: node_translation_target + field: translation_outdated + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: All + group: 1 + exposed: true + expose: + operator_id: '' + label: Outdated + description: '' + use_operator: false + operator: translation_outdated_op + identifier: translation_outdated + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_field: content_translation_outdated + plugin_id: boolean + translation_status: + id: translation_status + table: node_translation + field: translation_status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '2' + group: 1 + exposed: true + expose: + operator_id: '' + label: Translation status + description: '' + use_operator: false + operator: translation_status_op + identifier: translation_status + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + translation_target_language: false + entity_type: node + plugin_id: translation_views_status + translation_default: + id: translation_default + table: node_translation_target + field: translation_default + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '0' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + 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: boolean + translation_status_1: + id: translation_status_1 + table: node_translation + field: translation_status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '0' + group: 2 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + plugin_id: translation_views_status + translation_source: + id: translation_source + table: node_translation_target + field: translation_source + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '1' + group: 2 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + 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: translation_views_source_equals_row + translation_status_2: + id: translation_status_2 + table: node_translation + field: translation_status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '0' + group: 3 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + plugin_id: translation_views_status + translation_outdated_1: + id: translation_outdated_1 + table: node_translation_target + field: translation_outdated + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '1' + group: 3 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + 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: boolean + 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 + title: 'Content Translation jobs' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + filter_groups: + operator: AND + groups: + 1: AND + 2: OR + 3: OR + cache_metadata: + max-age: 0 + 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: translate/content + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/sites/all/modules/contrib/views/translation_views/config/schema/translation_views.views.schema.yml b/sites/all/modules/contrib/views/translation_views/config/schema/translation_views.views.schema.yml new file mode 100644 index 000000000..5b2eb4188 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/config/schema/translation_views.views.schema.yml @@ -0,0 +1,6 @@ +views.filter.translation_views_translation_count: + type: views.filter.numeric + label: 'Translation count' + +views.filter_value.translation_views_translation_count: + type: views.filter_value.numeric diff --git a/sites/all/modules/contrib/views/translation_views/src/EntityInfo.php b/sites/all/modules/contrib/views/translation_views/src/EntityInfo.php new file mode 100644 index 000000000..eafb54d6d --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/EntityInfo.php @@ -0,0 +1,78 @@ +entity = $entity; + $this->access = $access; + + $this->rowLanguage = $row_lang; + $this->currentLanguage = $language; + + $this->entityTypeId = $entity->getEntityTypeId(); + $this->entityType = $entity->getEntityType(); + $this->translations = $entity->getTranslationLanguages(); + $this->translatable = $entity->isTranslatable(); + } + + /** + * An wrapper to get translation access by particular entity per operation. + * + * @see \Drupal\content_translation\ContentTranslationHandlerInterface::getTranslationAccess, + * for available operations. + */ + public function getTranslationAccess($op) { + $access = $this->access->getTranslationAccess($this->entity, $op); + return $access->isAllowed(); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationCountField.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationCountField.php new file mode 100644 index 000000000..56fd5efea --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationCountField.php @@ -0,0 +1,53 @@ +entityTypeManager = $entity_type_manager; + $this->joinHandler = $join_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, $plugin_id, $plugin_definition, + $container->get('entity_type.manager'), + $container->get('plugin.manager.views.join') + ); + } + + /** + * {@inheritdoc} + */ + public function query() { + /** @var \Drupal\views\Plugin\views\query\Sql $query */ + $query = $this->query; + $join_alias = $this->joinLanguages($query); + $this->field_alias = $query->addField($join_alias, 'count_langs', $this->realField); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationOperationsField.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationOperationsField.php new file mode 100644 index 000000000..adbfa0842 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationOperationsField.php @@ -0,0 +1,241 @@ +getEntity($values); + $langcode_key = $this->buildSourceEntityLangcodeKey($entity); + $operations = $this->getOperations($entity, $values->{$langcode_key}); + + if ($this->options['destination']) { + foreach ($operations as &$operation) { + if (!isset($operation['query'])) { + $operation['query'] = []; + } + $operation['query'] += $this->getDestinationArray(); + } + } + $build = [ + '#type' => 'operations', + '#links' => $operations, + ]; + $build['#cache']['contexts'][] = 'url.query_args:target_language'; + + return $build; + } + + /** + * Build value key. + * + * Value key based on base table, + * and system name of langcode key (it might be differ then just 'langcode'), + * usually table alias is [entity_type]_field_data_[langcode_key]. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * Used to extract entity type info from entity. + * + * @return string + * The value key. + */ + protected function buildSourceEntityLangcodeKey(ContentEntityInterface $entity) { + return implode('_', [ + $this->view->storage->get('base_table'), + $entity->getEntityType()->getKey('langcode'), + ]); + } + + /** + * Operation links manager. + * + * Decide which links we should generate: + * based on user permissions, + * and entity state (has translation, is default, etc.). + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The source entity to get context for decision. + * @param string $row_lang + * The target langcode of the row. + * + * @return array + * Operation links' render array. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Core\Entity\EntityMalformedException + */ + protected function getOperations(ContentEntityInterface $entity, $row_lang) { + $links = []; + $current = $this->getTargetLanguage() + ? $this->getTargetLanguage() + : $row_lang; + + /* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */ + $handler = $this->getEntityManager() + ->getHandler($entity->getEntityTypeId(), 'translation'); + + // Construct special object to store common properties, + // it will be used by all builder functions, + // just as "trait" but for methods. + $entity_info = new HelperEntityInfo( + $entity, + $handler, + $this->languageManager->getLanguage($current), + $this->languageManager->getLanguage($row_lang) + ); + + $is_source = $entity->getUntranslated()->language()->getId() === $entity_info->rowLanguage->getId(); + + // Build edit & delete link. + if (array_key_exists($current, $entity_info->translations)) { + // If the user is allowed to edit the entity we point the edit link to + // the entity form, otherwise if we are not dealing with the original + // language we point the link to the translation form. + if ($entity->access('update', NULL, TRUE)->isAllowed() + && $entity_info->entityType->hasLinkTemplate('edit-form') + ) { + $links += $this->buildEditLink($entity_info, 'entity'); + } + elseif (!$is_source && $entity_info->getTranslationAccess('update')) { + $links += $this->buildEditLink($entity_info, 'translation'); + } + + // Build delete link. + if (!$is_source) { + if ($entity->access('delete') + && $entity_info->entityType->hasLinkTemplate('delete-form') + ) { + $links += $this->buildDeleteLink($entity_info, 'entity'); + } + elseif ($entity_info->getTranslationAccess('update')) { + $links += $this->buildDeleteLink($entity_info, 'translation'); + } + } + } + // Build add link. + elseif (!empty($current) + && $entity_info->translatable + && $entity_info->getTranslationAccess('create') + ) { + // No such translation. + $links += $this->buildAddLink($entity_info); + } + return $links; + + } + + /** + * Add link builder. + */ + protected function buildAddLink(HelperEntityInfo $entity_info) { + $links = []; + + $add_url = new Url( + "entity.{$entity_info->entityTypeId}.content_translation_add", + [ + 'source' => $entity_info->rowLanguage->getId(), + 'target' => $entity_info->currentLanguage->getId(), + $entity_info->entityTypeId => $entity_info->entity->id(), + ], + [ + 'language' => $entity_info->currentLanguage, + ] + ); + + $links['add'] = [ + 'title' => $this->t('Add'), + 'url' => $add_url, + ]; + return $links; + } + + /** + * Delete link builder. + */ + protected function buildDeleteLink(HelperEntityInfo $entity_info, $type = FALSE) { + $links = []; + + if (!$type) { + return $links; + } + + if ($type == 'entity') { + $links['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => $entity_info->entity->urlInfo('delete-form'), + 'language' => $entity_info->rowLanguage, + ]; + } + elseif ($type == 'translation') { + $links['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => new Url( + "entity.{$entity_info->entityTypeId}.content_translation_delete", + [ + 'language' => $entity_info->rowLanguage->getId(), + $entity_info->entityTypeId => $entity_info->entity->id(), + ], + [ + 'language' => $entity_info->rowLanguage, + ] + ), + ]; + } + return $links; + } + + /** + * Edit link builder. + * + * @throws \Drupal\Core\Entity\EntityMalformedException + */ + protected function buildEditLink(HelperEntityInfo $entity_info, $type = FALSE) { + $links = []; + + if ($type == 'entity') { + $links['edit']['url'] = $entity_info->entity->toUrl('edit-form'); + $links['edit']['language'] = $entity_info->rowLanguage; + } + elseif ($type == 'translation') { + $links['edit']['url'] = new Url( + "entity.{$entity_info->entityTypeId}.content_translation_edit", + [ + 'language' => $entity_info->rowLanguage->getId(), + $entity_info->entityTypeId => $entity_info->entity->id(), + ], + [ + 'language' => $entity_info->rowLanguage, + ] + ); + ; + } + + if (isset($links['edit'])) { + $links['edit']['title'] = $this->t('Edit'); + } + + return $links; + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationSourceLangcodeEqualsRowLangcodeField.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationSourceLangcodeEqualsRowLangcodeField.php new file mode 100644 index 000000000..176b8cf19 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationSourceLangcodeEqualsRowLangcodeField.php @@ -0,0 +1,49 @@ +field_alias = 'source_lang_eq_row'; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['type'] = ['default' => 'status']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function query() { + $table_alias = $this->ensureMyTable(); + $base_table = $this->view->storage->get('base_table'); + + $this->query->addField( + NULL, + "IF($table_alias.content_translation_source = $base_table.langcode, 1, 0)", + 'source_lang_eq_row' + ); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationStatus.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationStatus.php new file mode 100644 index 000000000..cc1b19cc1 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationStatus.php @@ -0,0 +1,95 @@ +get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['type'] = ['default' => 'status']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { + $this->definition['output formats']['status'] = [ + t('Translated'), $this->t('Not translated'), + ]; + $this->field_alias = 'translation_status'; + + parent::init($view, $display, $options); + } + + /** + * {@inheritdoc} + * + * Convert translation status value into TranslationStatusInterface values. + */ + public function render(ResultRow $values) { + $values->{$this->field_alias} = $values->{$this->field_alias} > 0 + ? TranslationStatusInterface::TRANSLATION_EXISTING + : NULL; + return parent::render($values); + } + + /** + * {@inheritdoc} + * + * Expression wouldn't work without: + * + * @see translation_views_query_views_alter() + */ + public function query() { + $table_alias = $this->ensureMyTable(); + $this->query->addField( + NULL, + "FIND_IN_SET(:langcode, $table_alias.langs) > 0", + 'translation_status', + [ + 'placeholders' => [':langcode' => '***TRANSLATION_VIEWS_TARGET_LANG***'], + ] + ); + } + +} \ No newline at end of file diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationTargetLanguageField.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationTargetLanguageField.php new file mode 100644 index 000000000..468a63b9d --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/field/TranslationTargetLanguageField.php @@ -0,0 +1,84 @@ +get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function usesGroupBy() { + return FALSE; + } + + /** + * {@inheritdoc} + * + * Override parent to get langcode from exposed input, + * instead from ResultRow. + */ + public function render(ResultRow $values) { + $target_lang = $this->getTargetLanguage(); + + $languages = $this->options['native_language'] + ? $this->languageManager->getNativeLanguages() + : $this->languageManager->getLanguages(); + + $build['#markup'] = $languages[$target_lang]->getName(); + $build['#cache']['contexts'][] = 'url.query_args:' . self::$targetExposedKey; + + return $build; + } + + /** + * {@inheritdoc} + */ + public function query() {} + + /** + * {@inheritdoc} + */ + public function clickSortable() { + return FALSE; + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationCountFilter.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationCountFilter.php new file mode 100644 index 000000000..7de7cf9f7 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationCountFilter.php @@ -0,0 +1,62 @@ +get('entity_type.manager'), + $container->get('plugin.manager.views.join') + ); + } + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ViewsHandlerManager $join_handler) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->entityTypeManager = $entity_type_manager; + $this->joinHandler = $join_handler; + } + + /** + * {@inheritdoc} + */ + public function query() { + /** @var \Drupal\views\Plugin\views\query\Sql $query */ + $query = $this->query; + $op = $this->operator; + + $join_alias = $this->joinLanguages($query); + + $query->addWhereExpression( + $this->options['group'], + "IF(ISNULL($join_alias.count_langs), 0, $join_alias.count_langs) $op :value", + [':value' => $this->value['value']] + ); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationSourceLangcodeEqualsRowLangcodeFilter.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationSourceLangcodeEqualsRowLangcodeFilter.php new file mode 100644 index 000000000..a1018f28a --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationSourceLangcodeEqualsRowLangcodeFilter.php @@ -0,0 +1,31 @@ +ensureMyTable(); + $base_table = $this->view->storage->get('base_table'); + + $this->query->addWhereExpression( + $this->options['group'], + "IF($table_alias.content_translation_source $this->operator $base_table.langcode, 1, 0) = :value", [ + ':value' => $this->value, + ] + ); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationStatusFilter.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationStatusFilter.php new file mode 100644 index 000000000..7fcaca632 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationStatusFilter.php @@ -0,0 +1,134 @@ +options['expose']['identifier'])) { + $value_id = $this->options['expose']['identifier']; + } + else { + $value_id = 'translation_views_status'; + } + + unset($form[$value_id]['#options']['All']); + } + + /** + * {@inheritdoc} + */ + protected function valueForm(&$form, FormStateInterface $form_state) { + parent::valueForm($form, $form_state); + + $exposed = $form_state->get('exposed'); + + if (!empty($this->options['exposed'])) { + $identifier = $this->options['expose']['identifier']; + $user_input = $form_state->getUserInput(); + + // We need set exposed input to our "BOTH" value, + // when there is no selected value by user yet, + // usually when there is no any changes in exposed filters. + if ($exposed && !isset($user_input[$identifier])) { + $user_input = $form_state->getUserInput(); + $user_input[$identifier] = self::VALUE_BOTH; + + $form_state->setUserInput($user_input); + $this->view->setExposedInput($user_input); + } + } + + $form['value'] = [ + '#type' => 'select', + '#title' => $this->t('Status'), + '#options' => [ + self::VALUE_BOTH => t('Both'), + self::VALUE_UNTRANSLATED => t('Not translated'), + self::VALUE_TRANSLATED => t('Translated'), + ], + '#multiple' => FALSE, + '#default_value' => $this->value, + ]; + } + + /** + * {@inheritdoc} + */ + public function query() { + $table_alias = $this->ensureMyTable(); + + $status = is_array($this->value) + ? (int) reset($this->value) + : $this->value; + + // We don't want to build and run query, + // when user specified both statuses. + if ($status == self::VALUE_BOTH) { + return FALSE; + } + + if ($status == self::VALUE_UNTRANSLATED) { + $op = '='; + } + // Then self::VALUE_TRANSLATED is the case. + else { + // Mysql FIND_IN_SET func will return position of element, + // when no element was found then it returns 0, + // so we use "> 0" as condition to filter out untranslated rows. + $op = '>'; + $status = 0; + } + + /* @var \Drupal\views\Plugin\views\query\Sql */ + $this->query->addWhereExpression( + $this->options['group'], + "FIND_IN_SET(:langcode, $table_alias.langs) $op :status", [ + ':langcode' => '***TRANSLATION_VIEWS_TARGET_LANG***', + ':status' => $status, + ] + ); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationTargetLanguageFilter.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationTargetLanguageFilter.php new file mode 100644 index 000000000..1bcc7b4f9 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/filter/TranslationTargetLanguageFilter.php @@ -0,0 +1,151 @@ +get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function buildExposeForm(&$form, FormStateInterface $form_state) { + parent::buildExposeForm($form, $form_state); + + unset($form['expose']['multiple']); + unset($form['expose']['required']); + + $form['expose']['identifier'] = [ + '#type' => 'hidden', + '#value' => $this->options['expose']['identifier'], + '#title' => $this->t('Filter identifier'), + '#size' => 40, + '#description' => $this->t('This will appear in the URL after the ? to identify this filter. Cannot be blank. Only letters, digits and the dot ("."), hyphen ("-"), underscore ("_"), and tilde ("~") characters are allowed.'), + ]; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['expose']['identifier'] = static::$targetExposedKey; + $options['value']['default'] = ''; + $options['remove']['default'] = FALSE; + + return $options; + } + + /** + * {@inheritdoc} + */ + protected function valueForm(&$form, FormStateInterface $form_state) { + parent::valueForm($form, $form_state); + + $exposed = $form_state->get('exposed'); + + if (!empty($this->options['exposed'])) { + $identifier = $this->options['expose']['identifier']; + $user_input = $form_state->getUserInput(); + + // We need set exposed input when there is no selected value by user yet. + if ($exposed && !isset($user_input[$identifier])) { + $value = PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT; + $this->setExposedValue($identifier, $value, $form_state); + } + } + + $form['value'] = [ + '#type' => 'select', + '#title' => $this->t('Target language'), + '#options' => $this->buildLanguageOptions(), + '#multiple' => FALSE, + '#weight' => 0, + '#default_value' => $this->value, + ]; + + if (!$exposed) { + $form['remove'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Remove rows where source language is equal to target language.'), + '#default_value' => $this->options['remove'], + '#weight' => -50, + ]; + } + } + + /** + * Provide options for langcode dropdown. + * + * Options are based on configurable languages or site default one. + */ + protected function buildLanguageOptions() { + $site_default = $this->languageManager->getDefaultLanguage(); + $options = $this->listLanguages(LanguageInterface::STATE_CONFIGURABLE); + + if (isset($options[$site_default->getId()])) { + unset($options[$site_default->getId()]); + } + + return [PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT => $site_default->getName()] + $options; + } + + /** + * {@inheritdoc} + */ + public function query() { + if ($this->options['remove']) { + $this->query->addWhere( + $this->options['group'], + $this->view->storage->get('base_table') . '.langcode', + '***TRANSLATION_VIEWS_TARGET_LANG***', + '<>' + ); + } + } + + /** + * Special setter for exposed value in views. + */ + protected function setExposedValue($identifier, $value, FormStateInterface $form_state) { + $user_input = $form_state->getUserInput(); + $user_input[$identifier] = $value; + + $form_state->setUserInput($user_input); + $this->view->setExposedInput($user_input); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/Plugin/views/join/TranslationLanguageJoin.php b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/join/TranslationLanguageJoin.php new file mode 100644 index 000000000..e92604f52 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/Plugin/views/join/TranslationLanguageJoin.php @@ -0,0 +1,68 @@ +database = $database; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('database') + ); + } + + /** + * {@inheritdoc} + */ + public function buildJoin($select_query, $table, $view_query) { + $query = $this->database->select($this->table, 'nfd'); + $query->fields('nfd', ['nid']); + + if (!empty($this->configuration['langcodes_as_count'])) { + $query->addExpression("COUNT(nfd.langcode)", 'count_langs'); + } + else { + $query->addExpression("GROUP_CONCAT(nfd.langcode separator ',')", 'langs'); + } + + if (!empty($this->configuration['exclude_default_langcode'])) { + $query->where('nfd.default_langcode != 1'); + } + + $query->groupBy('nfd.nid'); + $this->configuration['table formula'] = $query; + + parent::buildJoin($select_query, $table, $view_query); + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/TranslationCountTrait.php b/sites/all/modules/contrib/views/translation_views/src/TranslationCountTrait.php new file mode 100644 index 000000000..5bba6a3ff --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/TranslationCountTrait.php @@ -0,0 +1,58 @@ +tables[$this->tableAlias])) { + $query_base_table = $this->relationship ?: $this->view->storage->get('base_table'); + + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityType()); + $keys = $entity_type->getKeys(); + + $definition = [ + 'type' => 'LEFT', + 'left_field' => $keys['id'], + 'field' => $keys['id'], + 'table' => $query_base_table, + 'left_table' => $query_base_table, + 'exclude_default_langcode' => TRUE, + 'langcodes_as_count' => TRUE, + ]; + + $tableAlias = $query->ensureTable($query_base_table, $this->relationship); + $join = $this->joinHandler->createInstance('translation_views_language_join', $definition); + + return $query->addTable($query_base_table, $tableAlias, $join, $this->tableAlias); + } + else { + return $this->tableAlias; + } + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/TranslationViewsFieldDefinitions.php b/sites/all/modules/contrib/views/translation_views/src/TranslationViewsFieldDefinitions.php new file mode 100644 index 000000000..18fea1e7d --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/TranslationViewsFieldDefinitions.php @@ -0,0 +1,147 @@ + [ + 'field_name' => 'status', + 'real field' => 'langcode', + 'title' => t('Translation status'), + 'field' => [ + 'id' => 'translation_views_status', + 'help' => t('A boolean indicating whether the node is translated into target language.'), + 'additional fields' => [ + 'content_translation_source' => 'content_translation_source', + ], + ], + 'filter' => [ + 'id' => 'translation_views_status', + 'help' => t('A boolean indicating whether the node is translated into target language.'), + ], + ], + ]; + } + + /** + * Translations count field definition. + */ + public static function buildCountField() { + return [ + 'translation_count' => [ + 'field_name' => 'counter', + 'real field' => 'translation_langs', + 'title' => t('Translation counter'), + 'field' => [ + 'id' => 'translation_views_translation_count', + 'help' => t('Amount of translations (without default language).'), + ], + 'filter' => [ + 'id' => 'translation_views_translation_count', + 'help' => t('Filter rows by amount of translations (without default language).'), + ], + ], + ]; + } + + /** + * Translation changed field definition. + */ + public static function buildChangedField($definition) { + $field = []; + $field['translation_changed'] = $definition; + $field['translation_changed']['real field'] = 'changed'; + $field['translation_changed']['field']['id'] = 'date'; + $field['translation_changed']['field']['help'] = t('The time that the node was last edited in target language.'); + $field['translation_changed']['title'] = t('Translation changed time'); + return $field; + } + + /** + * Translation outdated field definition. + */ + public static function buildOutdatedField($definition) { + $field = []; + $field['translation_outdated'] = $definition; + $field['translation_outdated']['real field'] = 'content_translation_outdated'; + $field['translation_outdated']['field']['id'] = 'boolean'; + $field['translation_outdated']['filter']['id'] = 'boolean'; + $field['translation_outdated']['field']['help'] = t('A boolean indicating whether the target language translation is outdated'); + $field['translation_outdated']['filter']['help'] = t('A boolean indicating whether the target language translation is outdated'); + $field['translation_outdated']['title'] = t('Translation outdated'); + return $field; + } + + /** + * Translation source field definition. + */ + public static function buildSourceField($definition) { + $field = []; + $field['translation_source'] = $definition; + $field['translation_source']['real field'] = 'content_translation_source'; + $field['translation_source']['field']['id'] = 'translation_views_source_equals_row'; + $field['translation_source']['filter']['id'] = 'translation_views_source_equals_row'; + $field['translation_source']['field']['help'] = t('A boolean indicating whether the translation source of target language is same as the row language'); + $field['translation_source']['filter']['help'] = t('A boolean indicating whether the translation source of target language is same as the row language'); + $field['translation_source']['title'] = t('Translation source equals row language'); + return $field; + } + + /** + * Translation default langcode field definition. + */ + public static function buildDefaultLangcodeField($definition) { + $field = []; + $field['translation_default'] = $definition; + $field['translation_default']['real field'] = 'default_langcode'; + $field['translation_default']['field']['id'] = 'boolean'; + $field['translation_default']['filter']['id'] = 'boolean'; + $field['translation_default']['filter']['accept null'] = TRUE; + $field['translation_default']['field']['help'] = t('A boolean indicating whether the target language is default language'); + $field['translation_default']['filter']['help'] = t('A boolean indicating whether the target language is default language'); + $field['translation_default']['title'] = t('Target language equals default language'); + return $field; + } + + /** + * Translation operations field definition. + */ + public static function buildOpLinksField() { + $field['translation_operations'] = [ + 'title' => t('Translation operations'), + 'help' => t('Provides links to perform translation operations.'), + 'field' => [ + 'id' => 'translation_views_operations', + ], + ]; + return $field; + } + + /** + * Translation target language field definition. + */ + public static function buildTargetLanguageField() { + $field['translation_target_language']['field'] = [ + 'title' => t('Target language'), + 'id' => 'translation_views_target_language', + 'help' => t('The target language.'), + ]; + $field['translation_target_language']['filter'] = [ + 'title' => t('Target language'), + 'id' => 'translation_views_target_language', + 'help' => t('Define the target language for other translation fields/filters. This filter should in most cases be used only as exposed filter.'), + ]; + return $field; + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/src/TranslationViewsTargetLanguage.php b/sites/all/modules/contrib/views/translation_views/src/TranslationViewsTargetLanguage.php new file mode 100644 index 000000000..035a6c6c8 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/src/TranslationViewsTargetLanguage.php @@ -0,0 +1,41 @@ +view->getExposedInput(); + $langcode = $inputs[self::$targetExposedKey]; + + return $langcode == PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT + ? $this->languageManager->getDefaultLanguage()->getId() + : $langcode; + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/translation_views.info.yml b/sites/all/modules/contrib/views/translation_views/translation_views.info.yml new file mode 100644 index 000000000..8446791a8 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/translation_views.info.yml @@ -0,0 +1,14 @@ +name: Translation Views +type: module +description: Create customized lists and queries of translations from your database. +# core: 8.x +package: Multilingual +dependencies: + - drupal:content_translation + - drupal:views + +# Information added by Drupal.org packaging script on 2018-02-13 +version: '8.x-1.0-alpha1' +core: '8.x' +project: 'translation_views' +datestamp: 1518516495 diff --git a/sites/all/modules/contrib/views/translation_views/translation_views.module b/sites/all/modules/contrib/views/translation_views/translation_views.module new file mode 100644 index 000000000..c5193819a --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/translation_views.module @@ -0,0 +1,114 @@ +getFormObject(); + + if (!($form_object instanceof ContentEntityFormInterface)) { + return; + } + + $entity = $form_object->getEntity(); + $op = $form_object->getOperation(); + + if ($entity instanceof ContentEntityInterface + && $entity->isTranslatable() + && count($entity->getTranslationLanguages()) > 1 + && in_array($op, ['edit', 'add', 'default'], TRUE) + ) { + $form['source_langcode']['submit']['#submit'][] = 'translation_views_content_form_fix_destination'; + } +} + +/** + * Submit callback for "source_langcode" button. + */ +function translation_views_content_form_fix_destination($form, FormStateInterface $form_state) { + /* @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */ + /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + /* @var \Symfony\Component\HttpFoundation\ParameterBag $query */ + $form_object = $form_state->getFormObject(); + $entity = $form_object->getEntity(); + $source = $form_state->getValue(['source_langcode', 'source']); + $query = \Drupal::service('request_stack')->getCurrentRequest()->query; + $options = []; + + // Remove destination parameter from request, + // to allow "Change" button works like desired, + // see similar issue here: https://www.drupal.org/node/2325463. + // @todo: Create core issue for the content_translation module. + if ($query->has('destination')) { + $options['query']['destination'] = $query->get('destination'); + $query->remove('destination'); + } + + $entity_type_id = $entity->getEntityTypeId(); + $form_state->setRedirect("entity.$entity_type_id.content_translation_add", [ + $entity_type_id => $entity->id(), + 'source' => $source, + 'target' => $form_object->getFormLangcode($form_state), + ], $options); +} + +/** + * Implements hook_query_TAG_alter(). + * + * Add support to use views query substitutions in expressions + * from hook_views_query_substitutions(). + * Expressions can be used in views fields (i.e addField()), + * and views orderBy formula (i.e addOrderBy()). + * + * @todo Remove this workaround when will be solved in drupal core. + */ +function translation_views_query_views_alter(AlterableInterface $query) { + $substitutions = $query->getMetaData('views_substitutions'); + $expressions =& $query->getExpressions(); + + foreach ($expressions as $field_id => &$item) { + foreach ($item['arguments'] as $replacement_key => $value) { + if (!is_array($value)) { + if (isset($substitutions[$value])) { + $expressions[$field_id]['arguments'][$replacement_key] = $substitutions[$value]; + } + } + else { + foreach ($value as $sub_key => $sub_value) { + if (isset($substitutions[$sub_value])) { + $expressions[$field_id]['arguments'][$replacement_key][$sub_key] = $substitutions[$sub_value]; + } + } + } + } + } +} diff --git a/sites/all/modules/contrib/views/translation_views/translation_views.views.inc b/sites/all/modules/contrib/views/translation_views/translation_views.views.inc new file mode 100644 index 000000000..89822d4ac --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/translation_views.views.inc @@ -0,0 +1,108 @@ +getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->hasHandlerClass('views_data') + && $entity_type->isTranslatable() + && $content_translation_manager->isEnabled($entity_type_id) + ) { + + try { + /** @var \Drupal\views\EntityViewsDataInterface $views_data */ + $views_data = \Drupal::entityTypeManager()->getHandler( + $entity_type_id, + 'views_data' + ); + } + catch (PluginNotFound $e) { + throw new PluginNotFound($entity_type_id, sprintf( + 'The "%s" entity type did not specify views_data handler class.', + $entity_type->getLabel() + )); + } + + $field_data_table = $views_data->getViewsTableForEntityType($entity_type); + + if (!empty($field_data_table)) { + $key = $entity_type_id . '_translation'; + $target_key = $key . '_target'; + $group = t('@entity_label translation', [ + '@entity_label' => $entity_type->getLabel(), + ]); + + $translations_table =& $data[$key]; + $translations_table_target =& $data[$target_key]; + + $pk = $entity_type->getKey('id'); + + $translations_table['table'] = [ + 'group' => $group, + 'entity type' => $entity_type_id, + 'join' => [ + $field_data_table => [ + 'join_id' => 'translation_views_language_join', + 'type' => 'LEFT', + 'left_field' => $pk, + 'field' => $pk, + 'table' => $field_data_table, + ], + ], + ]; + + $translations_table += FieldDefinitions::buildStatusField(); + $translations_table += FieldDefinitions::buildTargetLanguageField(); + $translations_table += FieldDefinitions::buildOpLinksField(); + $translations_table += FieldDefinitions::buildCountField(); + + $translations_table_target['table'] = [ + 'group' => $group, + 'join' => [ + $field_data_table => [ + 'type' => 'LEFT', + 'left_field' => $pk, + 'field' => $pk, + 'table' => $field_data_table, + 'extra' => [ + [ + 'field' => 'langcode', + 'value' => '***TRANSLATION_VIEWS_TARGET_LANG***', + ], + ], + ], + ], + ]; + + $translations_table_target += FieldDefinitions::buildOutdatedField( + $translations_table['content_translation_outdated'] + ); + $translations_table_target += FieldDefinitions::buildSourceField( + $translations_table['content_translation_source'] + ); + $translations_table_target += FieldDefinitions::buildDefaultLangcodeField( + $translations_table['default_langcode'] + ); + $translations_table_target += FieldDefinitions::buildChangedField( + $translations_table['changed'] + ); + } + } + } + +} diff --git a/sites/all/modules/contrib/views/translation_views/translation_views.views_execution.inc b/sites/all/modules/contrib/views/translation_views/translation_views.views_execution.inc new file mode 100644 index 000000000..df547dcb9 --- /dev/null +++ b/sites/all/modules/contrib/views/translation_views/translation_views.views_execution.inc @@ -0,0 +1,36 @@ +getExposedInput(); + $langcodes = array_keys(\Drupal::languageManager()->getLanguages( + LanguageInterface::STATE_CONFIGURABLE + )); + + if (isset($inputs['translation_target_language']) + && in_array($inputs['translation_target_language'], $langcodes) + ) { + $langcode = $inputs['translation_target_language']; + } + else { + $langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); + } + + $substitutions = [ + '***TRANSLATION_VIEWS_TARGET_LANG***' => $langcode, + ]; + + return $substitutions; +} diff --git a/sites/all/modules/figli/edlp_ajax/edlp_ajax.module b/sites/all/modules/figli/edlp_ajax/edlp_ajax.module index 14284b8e0..d3f3bd886 100644 --- a/sites/all/modules/figli/edlp_ajax/edlp_ajax.module +++ b/sites/all/modules/figli/edlp_ajax/edlp_ajax.module @@ -20,6 +20,7 @@ function edlp_ajax_page_attachments(array &$attachments) { $redirect = false; $current_path = \Drupal::service('path.current')->getPath(); + $current_language = \Drupal::languageManager()->getCurrentLanguage()->getId(); $is_front = \Drupal::service('path.matcher')->isFrontPage(); // do not redirect if not node, term, or custom routing @@ -33,10 +34,12 @@ function edlp_ajax_page_attachments(array &$attachments) { $redirect = true; } + // $redirect = false; $js_str = "var edlp = {\n sys_path:'".$current_path."',\n is_front:".($is_front ? 'true':'false').",\n redirect:".($redirect ? 'true':'false').",\n + lang_code:'".$current_language."',\n };"; $attachments['#attached']['html_head'][] = [ diff --git a/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/corpus.min.js b/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/corpus.min.js index 533c1f0de..0534046ee 100644 --- a/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/corpus.min.js +++ b/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/corpus.min.js @@ -1,39 +1,1006 @@ +/** + * @Author: Bachir Soussi Chiadmi + * @Date: 18-12-2017 + * @Email: bachir@figureslibres.io + * @Filename: corpus.js + * @Last modified by: bach + * @Last modified time: 20-12-2017 + * @License: GPL-V3 + */ -(function($,Drupal,drupalSettings){EdlpCorpus=function(){var _activated=true;var _$body=$('body');var _$container=_$body;var _$canvas=$('').appendTo(_$container);var _canvas=_$canvas[0];var _ctx=_canvas.getContext('2d');var _canvas_props={'margin_top':90,'margin_right':0,'margin_bottom':65,'margin_left':0};var _physics=new Physics();var _nodes=[];var _articles_nodes=[];var _no_articles_nodes=[];var _nodes_by_entries={};var _nodes_by_nid={};var _nodes_Nid_Id={};var _nodes_centered=[];var _nodes_centered_attractions=[];var _playlist=[];var _p_velocity_factor=0.5;var _mouse_in=true;var _m_pos={x:0,y:0};var _node_hover_id=-1;var _node_opened_id=-1;var _$entrees_block=$('#block-edlpentreesblock');var _$entrees_block_termlinks=$('a.term-link',_$entrees_block);var _$articles_link;var _node_pop_up;var _settings=drupalSettings.edlp_corpus;var _ecolors=_settings.colors;var _attracter,_repulser_top,_repulser_center,_repulser_bottom,_scrambler_TL,_scrambler_TR,_scrambler_BR,_scrambler_BL,_scrambler_CL,_scrambler_CR;function init(){initCanvas();if(_activated){loadCorpus();}else{_$canvas.addClass('de-activated');}};function initCanvas(){window.addEventListener('resize',onResizeCanvas,false);onResizeCanvas();};function onResizeCanvas(){_canvas.width=window.innerWidth;_canvas.height=window.innerHeight;for(var i=0;i<_nodes.length;i++){_nodes[i].onResizeCanvas();resizePhysics();}};function loadCorpus(){var ajax_path=_settings.load_corpus_ajax_url;var path=window.location.origin+Drupal.url(ajax_path);$.getJSON(path,{}).done(function(data){onCorpusLoaded(data);}).fail(function(jqxhr,textStatus,error){onCorpusLoadError(jqxhr,textStatus,error);});};function onCorpusLoadError(jqxhr,textStatus,error){void 0;};function onCorpusLoaded(data){initPhysics();buildNodes(data.nodes);initArtilesLink();initNodePopup();initEvents();checkPreOpenedEntry();startAnime();};function initPhysics(){_attracter=_physics.makeParticle(1000);_attracter.fixed=true;resizePhysics();};function resizePhysics(){_attracter.position={x:_canvas.width/2,y:_canvas.height/2};};function buildNodes(nodes){var d;for(var i in nodes){d=i<1?true:false;new Node(i,nodes[i],d);_playlist.push({'id':i,'nid':nodes[i].nid,'audio_url':nodes[i].audio_url});}};function Node(i,node,d){this.id=i;for(key in node) -this[key]=node[key];_nodes.push(this);_nodes_by_nid[this.nid]=this;_nodes_Nid_Id[this.nid]=this.id;if(this.has_article==1){_articles_nodes.push(this);}else{_no_articles_nodes.push(this);} -for(var j=0;j3:this.r=10;break;case this.entrees.length>1&&this.entrees.length<=3:this.r=7;break;default:this.r=4;break;} -this.e_color='e_col_'+this.entrees[Math.floor(Math.random(this.entrees.length))];this.hover=false;this.opened=false;this.faded=false;this.center=false;this.aside=false;this.scrambling=false;this.n_repulses={};if(typeof Node.initialized=="undefined"){Node.prototype.init=function(){this.calcWallLimits();this.x=this.wall_limits.left+Math.random()*(this.wall_limits.right-this.wall_limits.left);this.y=this.wall_limits.top+Math.random()*(this.wall_limits.bottom-this.wall_limits.top);this.ori_pos={x:this.x,y:this.y};this.initPhysics();};Node.prototype.initPhysics=function(){this.p=_physics.makeParticle(this.mass,this.x,this.y);this.p.velocity=new Physics.Vector((Math.random()-0.5)*_p_velocity_factor,(Math.random()-0.5)*_p_velocity_factor);this.ori_p=_physics.makeParticle(1000,this.ori_pos.x,this.ori_pos.y);this.ori_p.fixed=true;this.ori_p_attract=_physics.makeAttraction(this.ori_p,this.p,1000,_canvas.width*2);this.ori_p_attract.on=false;this.attract=_physics.makeAttraction(_attracter,this.p,1000,_canvas.width*2);this.attract.on=false;};Node.prototype.calcWallLimits=function(){this.wall_limits={top:_canvas_props.margin_top+this.r,right:_canvas.width-_canvas_props.margin_right-this.r,bottom:_canvas.height-_canvas_props.margin_bottom-this.r,left:_canvas_props.margin_left+this.r}};Node.prototype.onResizeCanvas=function(){this.calcWallLimits();this.shuffleOriP();};Node.prototype.shuffleOriP=function(){this.ori_pos.x=this.wall_limits.left+Math.random()*(this.wall_limits.right-this.wall_limits.left);this.ori_pos.y=this.wall_limits.top+Math.random()*(this.wall_limits.bottom-this.wall_limits.top);this.scramble();};Node.prototype.onUpdate=function(){if(this.hover||this.opened){this.p.fixed=true;}else{this.p.fixed=false;} -if(this.center) -this.limitEvolutionZone();if(!this.p.resting()){this.checkWallBouncing();this.updatePos();if(this.scrambling){this.checkOriPDist();}} -if(_mouse_in&&!this.aside) -this.checkMouse();this.draw();};Node.prototype.checkVelocityThreshold=function(){if(!this.centered||!this.aside){if(Math.abs(this.p.velocity.x)this.wall_limits.right&&this.p.velocity.x>0:this.p.position.x=this.wall_limits.right;this.p.velocity.multiplyScalarXY(-1,1).multiplyScalar(0.75);break;case this.ythis.wall_limits.bottom&&this.p.velocity.y>0:this.p.position.y=this.wall_limits.bottom;this.p.velocity.multiplyScalarXY(1,-1).multiplyScalar(0.75);break;}};Node.prototype.updatePos=function(){this.x=this.p.position.x;this.y=this.p.position.y;};Node.prototype.checkMouse=function(){if(_m_pos.x>this.x-this.r&&_m_pos.xthis.y-this.r&&_m_pos.y0.4){this.p.velocity.multiplyScalar(0.995);}} -if(Math.abs(_attracter.position.x-this.x)>_canvas.width/3||Math.abs(_attracter.position.y-this.y)>_canvas.height/3){this.attract.on=true;}else{this.attract.on=false;}} -Node.prototype.setAside=function(){this.aside=true;this.stopScrambling();this.unsetCentered();this.ori_p.position.x=this.x<_canvas.width/2?this.wall_limits.left:this.wall_limits.right;this.ori_p_attract.on=true;} -Node.prototype.unsetAside=function(){void 0;this.aside=false;this.ori_p_attract.on=false;this.ori_p.position.x=this.ori_pos.x;} -Node.prototype.scramble=function(){this.scrambling=true;this.unsetCentered();this.unsetAside();this.ori_p.position.x=this.ori_pos.x;this.ori_p.position.y=this.ori_pos.y;this.ori_p_attract.on=true;} -Node.prototype.checkOriPDist=function(){if(this.p.distanceTo(this.ori_p)<50){this.stopScrambling();}};Node.prototype.stopScrambling=function(){this.scrambling=false;this.ori_p_attract.on=false;} -Node.prototype.draw=function(){_ctx.beginPath();_ctx.globalAlpha=this.faded?0.1:1;_ctx.fillStyle=_ecolors[this.e_color];_ctx.fillRect(this.x-this.r,this.y-this.r,this.r*2,this.r*2);if(this.opened){_ctx.lineWidth='1px';_ctx.strokeStyle='rgb(255,0,0)';_ctx.strokeRect(this.x-this.r-3,this.y-this.r-3,this.r*2+6,this.r*2+6);} -_ctx.globalAlpha=1;_ctx.closePath();};Node.initialized=true;} -this.init();};function checkParticulesCollisions(){var na,nb,ma,mb,w,h,dx,dy,makeup,newVelX1,newVelY1,newVelX2,newVelY2;for(var n=0,l=_nodes.length;n300){if(Math.random()>0.3)continue;}} -w=h=(na.r+nb.r);dx=na.p.position.x-nb.p.position.x;dy=na.p.position.y-nb.p.position.y;if(Math.abs(dx)>w||Math.abs(dy)>h)continue;if(Math.abs(dx)0){na.p.position.y+=makeup;nb.p.position.y-=makeup;}else{na.p.position.y-=makeup;nb.p.position.y+=makeup;} -newVelY1=(ma-mb)/(ma+mb)*na.p.velocity.y+2*mb/(ma+mb)*nb.p.velocity.y;newVelY2=(mb-ma)/(mb+ma)*nb.p.velocity.y+2*ma/(mb+ma)*na.p.velocity.y;na.p.velocity.y=newVelY1;nb.p.velocity.y=newVelY2;}else{makeup=(w-Math.abs(dx))/2;if(dx>0){na.p.position.x+=makeup;nb.p.position.x-=makeup;}else{na.p.position.x-=makeup;nb.p.position.x+=makeup;} -newVelX1=(ma-mb)/(ma+mb)*na.p.velocity.x+2*mb/(ma+mb)*nb.p.velocity.x;newVelX2=(mb-ma)/(mb+ma)*nb.p.velocity.x+2*ma/(mb+ma)*na.p.velocity.x;na.p.velocity.x=newVelX1;nb.p.velocity.x=newVelX2;} -na.p.velocity.multiplyScalar(0.90);nb.p.velocity.multiplyScalar(0.90);}}};function openNodeByNid(nid){closeNode();if(typeof _nodes_Nid_Id[nid]!='undefined'){_node_opened_id=_nodes_Nid_Id[nid];_nodes[_nodes_Nid_Id[nid]].open();}} -function closeNode(){if(_node_opened_id!=-1){_nodes[_node_opened_id].close();_node_opened_id=-1;}} -function createNodesRepulsions(){purgeNodesRepulsions();for(var n=0;n<_nodes_centered.length;n++){for(var q=n+1;q<_nodes_centered.length;q++){_physics.makeAttraction(_nodes_centered[n].p,_nodes_centered[q].p,-0.4,10);}}};function purgeNodesRepulsions(){for(var a=_physics.attractions.length-1;a>=0;a--){if(Math.abs(_physics.attractions[a].constant)<1){_physics.attractions.splice(a,1);}else{break;}}};function toggleEntree(tid,bubbling){var $link=_$entrees_block_termlinks.filter('[tid="'+tid+'"]');var $li=$link.parents('li');var sys_path=$link.attr('data-drupal-link-system-path');var url=$link.attr('href');if(!$li.is('.opened')){_$entrees_block_termlinks.parents('li').removeClass('opened');$li.addClass('opened');filterEntree(tid);if(bubbling){_$body.trigger({'type':'open_entree','tid':tid,'url':url,'sys_path':sys_path});}}else if(bubbling){$li.removeClass('opened');scrambleCollection();_$body.trigger({'type':'close_entree','tid':tid});}};function filterEntree(t){shutDownArticles();_nodes_centered=[];for(var n=0;n<_nodes.length;n++){if(_nodes[n].entrees.indexOf(t)==-1){_nodes[n].setAside();}else{_nodes[n].setCenteredOnEntree(t);_nodes_centered.push(_nodes[n]);}} -createNodesRepulsions();};function highlightEntries(){_$entrees_block_termlinks.removeClass('highlighted');var id=-1;if(_node_hover_id!=-1){id=_node_hover_id;}else if(_node_opened_id!=-1){id=_node_opened_id;} -var entree;if(id!=-1){for(var i=0;i<_nodes[id].entrees.length;i++){entree=_nodes[id].entrees[i];_$entrees_block_termlinks.filter(function(){return $(this).attr('tid')==entree;}).addClass('highlighted');}}};function filterSearchResults(nids){shutDownArticles();_nodes_centered=[];for(var n=0;n<_nodes.length;n++){if(nids.indexOf(_nodes[n].nid)==-1){_nodes[n].setAside();}else{_nodes[n].setCentered();_nodes_centered.push(_nodes[n]);}} -createNodesRepulsions();};function scrambleCollection(){for(var i=0;i<_nodes.length;i++){_nodes[i].scramble();}};function closeAllEntries(){_$entrees_block_termlinks.each(function(index,el){if($(this).parents('li').is('.opened')){$(this).trigger('click');return false;}});};function initArtilesLink(){_$articles_link=$('').html('Articles').attr("href","#articles").addClass('articles-link').on('click',onCLickedOnArticles);$('.item-list ul',_$entrees_block).append($('
  • ').append($('').append(_$articles_link)));};function onCLickedOnArticles(e){e.preventDefault();$(this).toggleClass('is-active');if($(this).is('.is-active')){filterArticles();closeAllEntries();}else{resetArticlesFilter();} -return false;};function filterArticles(){for(var i=0;i<_no_articles_nodes.length;i++){_no_articles_nodes[i].fade();}};function resetArticlesFilter(){for(var i=0;i<_no_articles_nodes.length;i++){_no_articles_nodes[i].unFade();}};function shutDownArticles(){if(_$articles_link.is('.is-active')) -_$articles_link.trigger('click');};function initEvents(){void 0;_$canvas.on('mousemove',function(event){event.preventDefault();_m_pos.x=event.originalEvent.clientX;_m_pos.y=event.originalEvent.clientY;}).on('mouseenter',function(event){_mouse_in=true;}).on('mouseout',function(event){_mouse_in=false;_node_pop_up.removeNode();}).on('click',function(event){if(event.target.tagName!="A"&&event.target.tagName!="INPUT"){event.preventDefault();if(_node_hover_id!=-1){var event={'type':'corpus-cliked-on-node','target_node':{'id':_node_hover_id,'nid':_nodes[_node_hover_id].nid,'audio_url':_nodes[_node_hover_id].audio_url},};_$canvas.trigger(event);}else{_$canvas.trigger('corpus-cliked-on-map');}}}).on('audio-node-opened',function(e){openNodeByNid(e.nid);}).on('audio-node-closed',function(e){closeNode();}).on('open-entree',function(e){toggleEntree(e.tid);}).on('close-all-entree',function(e){closeAllEntries();});_$entrees_block_termlinks.on('click',function(event){event.preventDefault();toggleEntree($(this).attr('tid'),true);return false;});_$body.on('chutier-action-done',function(e){_nodes_by_nid[e.target_id].chutier_action=e.new_action;}).on('search-results-loaded',function(e){filterSearchResults(e.results);}).on('search-closed',function(e){scrambleCollection();}).on('new-content-not-entree-ajax-loaded',function(e){if(_$entrees_block_termlinks.parents('li.opened').length){_$entrees_block_termlinks.parents('li').removeClass('opened');scrambleCollection();}});};function checkPreOpenedEntry(){_$entrees_block.find('li.entree').each(function(index,el){var $li=$(this);if($('a.is-active',$li).length){$li.addClass('opened');filterEntree($li.attr('tid'));return false;}});};function initNodePopup(){_node_pop_up=new NodePopUp();};function NodePopUp(){this.visible=false;this.node;this.$dom=$('
    ').addClass('node-popup').attr('pos','top-right').appendTo('body');this.$content=$('
    ').addClass('inner').appendTo(this.$dom);if(typeof NodePopUp.initialized=="undefined"){NodePopUp.prototype.setNode=function(n){this.node=n;this.setPositioning();this.setContent();};NodePopUp.prototype.setPositioning=function(){switch(true){case this.node.x>this.node.wall_limits.right-350&&this.node.ythis.node.wall_limits.right-350:this.$dom.attr('pos','top-left');break;case this.node.y').addClass('entrees');for(var i=0;i').addClass('entree').attr('tid',tid));} -var $chutier_action=$('').addClass('chutier-icon').attr('action',this.node.chutier_action);this.$content.append($entrees).append('

    '+this.node.title+'

    ').append('
    '+this.node.description+'
    ').append($chutier_action);};NodePopUp.prototype.removeNode=function(){this.node=false;};NodePopUp.prototype.draw=function(){if(this.node){this.$dom.css({'display':"block",'left':this.node.x+"px",'top':this.node.y+"px",});}else{this.$dom.css({'display':"none",});}};NodePopUp.initialized=true;}} -function render(){_ctx.clearRect(0,0,_canvas.width,_canvas.height);checkParticulesCollisions();for(var i=0;i<_nodes.length;i++){_nodes[i].onUpdate();} -_node_pop_up.draw();if(_node_hover_id!=-1){_canvas.style.cursor='pointer';}else{_canvas.style.cursor='auto';} -highlightEntries();};function startAnime(){_physics.onUpdate(render);_physics.play() -$('body').attr('corpus','map-ready').trigger({'type':'corpus-map-ready','playlist':_playlist});};init();} -$(document).ready(function($){var edlpcorpus=new EdlpCorpus();});})(jQuery,Drupal,drupalSettings); \ No newline at end of file +// JS performance improvement http://archive.oreilly.com/pub/a/server-administration/excerpts/even-faster-websites/writing-efficient-javascript.html + +/** + * depends on : + * physics.js : http://jonobr1.com/Physics/# + */ + +(function($, Drupal, drupalSettings) { + + EdlpCorpus = function(){ + var _activated = true; + var _$body = $('body'); + var _$container = _$body; + var _$canvas = $('').appendTo(_$container); + var _canvas = _$canvas[0]; + var _ctx = _canvas.getContext('2d'); + var _canvas_props = { + 'margin_top':90, + 'margin_right':0, + 'margin_bottom':65, + 'margin_left':0 + }; + var _physics = new Physics(); + var _nodes = []; + var _articles_nodes = []; + var _no_articles_nodes = []; + var _nodes_by_entries = {}; + var _nodes_by_nid = {}; + var _nodes_Nid_Id = {}; + var _nodes_centered = []; + var _nodes_centered_attractions = []; + + var _playlist = []; + + var _p_velocity_factor = 0.5; + var _mouse_in = true; + var _m_pos = {x:0, y:0}; + var _node_hover_id = -1; + var _node_opened_id = -1; + var _$entrees_block = $('#block-edlpentreesblock'); + var _$entrees_block_termlinks = $('a.term-link', _$entrees_block); + var _$articles_link; + var _node_pop_up; + // Colors depend on drupalSettings loaded by edlp_corpus.module + var _settings = drupalSettings.edlp_corpus; + var _ecolors = _settings.colors; + // Physics + var _attracter, + _repulser_top, + _repulser_center, + _repulser_bottom, + _scrambler_TL, + _scrambler_TR, + _scrambler_BR, + _scrambler_BL, + _scrambler_CL, + _scrambler_CR; + + // ____ _ __ + // / _/___ (_) /______ + // / // __ \/ / __/ ___/ + // _/ // / / / / /_(__ ) + // /___/_/ /_/_/\__/____/ + function init(){ + initCanvas(); + if(_activated){ + loadCorpus(); + }else{ + _$canvas.addClass('de-activated'); + } + }; + + // ___ + // / __|__ _ _ ___ ____ _ ___ + // | (__/ _` | ' \ V / _` (_-< + // \___\__,_|_||_\_/\__,_/__/ + function initCanvas(){ + // resize the canvas to fill browser window dynamically + window.addEventListener('resize', onResizeCanvas, false); + onResizeCanvas(); + }; + function onResizeCanvas() { + _canvas.width = window.innerWidth; + _canvas.height = window.innerHeight; + for (var i = 0; i < _nodes.length; i++) { + _nodes[i].onResizeCanvas(); + // move _attracter and _repulser to the center again + resizePhysics(); + } + }; + + // ___ + // / __|___ _ _ _ __ _ _ ___ + // | (__/ _ \ '_| '_ \ || (_-< + // \___\___/_| | .__/\_,_/__/ + // |_| + function loadCorpus(){ + // console.log('drupalSettings',drupalSettings); + // console.log('window.location', window.location); + // var path = window.location.origin + drupalSettings.basepath + "edlp/corpus"; + var ajax_path = _settings.load_corpus_ajax_url; + var path = window.location.origin + Drupal.url(ajax_path); + $.getJSON(path, {}) + .done(function(data){ + onCorpusLoaded(data); + }) + .fail(function(jqxhr, textStatus, error){ + onCorpusLoadError(jqxhr, textStatus, error); + }); + }; + function onCorpusLoadError(jqxhr, textStatus, error){ + console.warn('corpus load failed', jqxhr.responseText); + }; + function onCorpusLoaded(data){ + //console.log('corpus loaded : data', data); + // console.log('first node', data.nodes[0]); + // buildParticles(data.nodes); + initPhysics(); + buildNodes(data.nodes); + initArtilesLink(); + initNodePopup(); + initEvents(); + checkPreOpenedEntry(); + startAnime(); + }; + + // ___ _ _ + // | _ \ |_ _ _ __(_)__ ___ + // | _/ ' \ || (_-< / _(_-< + // |_| |_||_\_, /__/_\__/__/ + // |__/ + function initPhysics(){ + _attracter = _physics.makeParticle(1000); + _attracter.fixed = true; + // move _attracter on window resize + resizePhysics(); + }; + function resizePhysics(){ + // attracters + _attracter.position = {x:_canvas.width/2, y:_canvas.height/2}; + }; + + // _ _ _ + // | \| |___ __| |___ ___ + // | .` / _ \/ _` / -_|_-< + // |_|\_\___/\__,_\___/__/ + function buildNodes(nodes){ + //console.log("buildNodes", nodes); + var d; + for (var i in nodes) { + d = i < 1 ? true : false; + // _nodes.push(new Node(i,nodes[i],d)); + new Node(i,nodes[i],d); + + _playlist.push({ + 'id':i, + 'nid':nodes[i].nid, + 'audio_url':nodes[i].audio_url + }); + } + //console.log('_nodes',_nodes); + //console.log('_articles_nodes',_articles_nodes); + //console.log('_no_articles_nodes',_no_articles_nodes); + //console.log('_nodes_by_entries', _nodes_by_entries); + + //console.log('_physics.attractions.length', _physics.attractions.length); + }; + + function Node(i,node,d){ + this.id = i; + for(key in node) + this[key] = node[key]; + + // record the node in different lists + _nodes.push(this); + + _nodes_by_nid[this.nid] = this; + _nodes_Nid_Id[this.nid] = this.id; + + if(this.has_article == 1){ + _articles_nodes.push(this); + }else{ + _no_articles_nodes.push(this); + } + + for (var j = 0; j < this.entrees.length; j++) { + if(typeof _nodes_by_entries[this.entrees[j]] == 'undefined') + _nodes_by_entries[this.entrees[j]] = []; + + _nodes_by_entries[this.entrees[j]].push(this); + } + + this.debug = d; + this.mass = Math.max(1, this.entrees.length); //1; + this.velocity_threshold = 0.01; + // define radius regarding entries length + switch(true){ + case this.entrees.length > 3: + this.r = 10; + break; + case this.entrees.length > 1 && this.entrees.length <= 3: + this.r = 7; + break; + default: + this.r = 4; + break; + } + + this.e_color = 'e_col_'+this.entrees[Math.floor(Math.random(this.entrees.length))]; + + this.hover = false; + this.opened = false; + this.faded = false; + this.center = false; + this.aside = false; + this.scrambling = false; + + this.n_repulses = {}; + + // prototypes + if (typeof Node.initialized == "undefined") { + + Node.prototype.init = function(){ + this.calcWallLimits(); + + this.x = this.wall_limits.left + Math.random()*(this.wall_limits.right - this.wall_limits.left); + this.y = this.wall_limits.top + Math.random()*(this.wall_limits.bottom - this.wall_limits.top); + + this.ori_pos = {x:this.x,y:this.y}; + // TODO: don't forget to move ori_pos on window resize + + this.initPhysics(); + + }; + + Node.prototype.initPhysics = function(){ + // particule + this.p = _physics.makeParticle(this.mass, this.x, this.y); + this.p.velocity = new Physics.Vector((Math.random()-0.5)*_p_velocity_factor, (Math.random()-0.5)*_p_velocity_factor); + // origin position particule + this.ori_p = _physics.makeParticle(1000, this.ori_pos.x, this.ori_pos.y); + this.ori_p.fixed = true; + // TODO: don't forget to move ori_p on window resize + this.ori_p_attract = _physics.makeAttraction(this.ori_p, this.p, 1000, _canvas.width*2); + this.ori_p_attract.on = false; + // attracter + this.attract = _physics.makeAttraction(_attracter, this.p, 1000, _canvas.width*2); + this.attract.on = false; + }; + + Node.prototype.calcWallLimits = function(){ + this.wall_limits = { + top: _canvas_props.margin_top +this.r, + right: _canvas.width -_canvas_props.margin_right -this.r, + bottom: _canvas.height -_canvas_props.margin_bottom -this.r, + left: _canvas_props.margin_left +this.r + } + }; + + Node.prototype.onResizeCanvas = function(){ + this.calcWallLimits(); + this.shuffleOriP(); + // TODO: when canvas is smaller what about nodes hors champs, they are coming back alone but it's too long + }; + + Node.prototype.shuffleOriP = function(){ + this.ori_pos.x = this.wall_limits.left + Math.random()*(this.wall_limits.right - this.wall_limits.left); + this.ori_pos.y = this.wall_limits.top + Math.random()*(this.wall_limits.bottom - this.wall_limits.top); + this.scramble(); + }; + + Node.prototype.onUpdate = function(){ + + // this.p.velocity.multiplyScalar(0.99); + if(this.hover || this.opened){ + this.p.fixed = true; + }else{ + this.p.fixed = false; + } + + if(this.center) + this.limitEvolutionZone(); + + if(!this.p.resting()){ + // this.checkVelocityThreshold(); + this.checkWallBouncing(); + this.updatePos(); + if(this.scrambling){ + this.checkOriPDist(); + } + } + + if(_mouse_in && !this.aside) + this.checkMouse(); + + // if(this.debug) + // console.log("Node pos: ", {x:this.x, y:this.y}); + this.draw(); + }; + + Node.prototype.checkVelocityThreshold = function(){ + if(!this.centered || !this.aside){ + if (Math.abs(this.p.velocity.x) < this.velocity_threshold + && Math.abs(this.p.velocity.y) < this.velocity_threshold){ + this.p.velocity.multiplyScalar(0); + // this.p.makeFixed(); + } + } + }; + + Node.prototype.checkWallBouncing = function(){ + switch(true){ + case this.x < this.wall_limits.left && this.p.velocity.x < 0: + this.p.position.x = this.wall_limits.left; + this.p.velocity.multiplyScalarXY(-1, 1).multiplyScalar(0.75); + break; + case this.x > this.wall_limits.right && this.p.velocity.x > 0: + this.p.position.x = this.wall_limits.right; + this.p.velocity.multiplyScalarXY(-1, 1).multiplyScalar(0.75); + break; + case this.y < this.wall_limits.top && this.p.velocity.y < 0 : + this.p.position.y = this.wall_limits.top; + this.p.velocity.multiplyScalarXY(1,-1).multiplyScalar(0.75); + break; + case this.y > this.wall_limits.bottom && this.p.velocity.y > 0 : + this.p.position.y = this.wall_limits.bottom; + this.p.velocity.multiplyScalarXY(1,-1).multiplyScalar(0.75); + break; + } + }; + + Node.prototype.updatePos = function(){ + this.x = this.p.position.x; + this.y = this.p.position.y; + }; + + Node.prototype.checkMouse = function(){ + + if( _m_pos.x > this.x - this.r + && _m_pos.x < this.x + this.r + && _m_pos.y > this.y - this.r + && _m_pos.y < this.y + this.r){ + if(_node_hover_id == -1 || _node_hover_id !== this.id){ + // console.log("Node hover", this.id); + this.hover = true; + _node_hover_id = this.id; + _node_pop_up.setNode(this); + } + }else{ + this.hover = false; + if (_node_hover_id == this.id) { + _node_hover_id = -1; + _node_pop_up.removeNode(); + } + } + }; + + Node.prototype.open = function(){ + this.opened = true; + _node_opened_id = this.id; + }; + Node.prototype.close = function(){ + this.opened = false; + }; + + Node.prototype.fade = function(){ + this.faded = true; + }; + Node.prototype.unFade = function(){ + this.faded = false; + }; + + Node.prototype.setCenteredOnEntree = function(tid){ + this.e_color = 'e_col_'+tid; + this.setCentered(); + }; + + Node.prototype.setCentered = function(){ + this.center = true; + this.stopScrambling(); + this.unsetAside(); + this.attract.on = true; + } + Node.prototype.unsetCentered = function(){ + this.center = false; + this.attract.on = false; + } + + Node.prototype.limitEvolutionZone = function(){ + // smouthly slow down node near center + // if(this.id == 1) + // console.log('this.p.velocity', this.p.velocity); + this.dist_to_attracter = this.p.distanceTo(_attracter); + if( this.dist_to_attracter < _canvas.width/4){ + if( this.p.velocity.length() > 0.4 ){ + this.p.velocity.multiplyScalar(0.995); + } + } + if( Math.abs(_attracter.position.x - this.x) > _canvas.width/3 + || Math.abs(_attracter.position.y - this.y) > _canvas.height/3){ + this.attract.on = true; + }else{ + this.attract.on = false; + } + } + + Node.prototype.setAside = function(){ + this.aside = true; + // this.fade(); + this.stopScrambling(); + this.unsetCentered(); + this.ori_p.position.x = this.x < _canvas.width /2 ? this.wall_limits.left : this.wall_limits.right; + this.ori_p_attract.on = true; + } + Node.prototype.unsetAside = function(){ + console.log('unsetAside'); + this.aside = false; + this.ori_p_attract.on = false; + this.ori_p.position.x = this.ori_pos.x; + // this.unFade(); + } + + Node.prototype.scramble = function(){ + this.scrambling = true; + this.unsetCentered(); + this.unsetAside(); + this.ori_p.position.x = this.ori_pos.x; + this.ori_p.position.y = this.ori_pos.y; + this.ori_p_attract.on = true; + } + Node.prototype.checkOriPDist = function(){ + if(this.p.distanceTo(this.ori_p) < 50){ + this.stopScrambling(); + } + }; + Node.prototype.stopScrambling = function(){ + this.scrambling = false; + this.ori_p_attract.on = false; + // this.scramble_TL.on = false; + // this.scramble_TR.on = false; + // this.scramble_BR.on = false; + // this.scramble_BL.on = false; + // this.scramble_CR.on = false; + // this.scramble_CL.on = false; + } + + Node.prototype.draw = function(){ + // carre plein + // clouleur aléatoire ds les entrees + // 3 tailles : + // - 1 entree : petit carre 5px + // - 2-3 entrees : moyen 7.5px + // - >3 entrees : grand 10px + // actif entouré de rouge + + _ctx.beginPath(); + _ctx.globalAlpha = this.faded ? 0.1 : 1; + // _ctx.fillStyle = !this.p.resting() ? _ecolors[this.e_color] : 'rgb(0, 0, 0)'; + _ctx.fillStyle = _ecolors[this.e_color]; + _ctx.fillRect(this.x - this.r,this.y - this.r,this.r*2,this.r*2); + + if(this.opened){ + _ctx.lineWidth = '1px'; + _ctx.strokeStyle = 'rgb(255,0,0)'; + _ctx.strokeRect(this.x - this.r-3,this.y - this.r-3,this.r*2+6,this.r*2+6); + } + _ctx.globalAlpha = 1; + _ctx.closePath(); + }; + + Node.initialized = true; + } + + this.init(); + }; + // TODO: we may convert a lot of computation into a web worker + // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers + function checkParticulesCollisions(){ + // pre create vars to save memory; + var na,nb, + ma,mb, + w,h,dx,dy, + makeup, + newVelX1, newVelY1, newVelX2, newVelY2; + // , angle; + // colisions between _particules + for (var n = 0, l = _nodes.length; n < l; n++) { + na = _nodes[n]; + ma = na.p.mass; + // avoid colliding for centered nodes + // if(_nodes[n].center) continue; + + // avoid colliding for scrambling nodes + if(na.scrambling) continue; + + for (var q = n+1; q < l; q++) { + nb = _nodes[q]; + mb = nb.p.mass; + // avoid impact between center and aside particules + if((na.center && nb.aside) || (na.aside && nb.center)) + continue; + + // avoid impact between two centered particulses that comes to the center + if(na.center && nb.center){ + if(Math.min(na.p.distanceTo(_attracter), nb.p.distanceTo(_attracter)) > 300){ + if( Math.random()>0.3 ) continue; + } + } + + w = h = (na.r+nb.r); + dx = na.p.position.x - nb.p.position.x; + dy = na.p.position.y - nb.p.position.y; + + // if both dx and dy are inferior to w & h so squares are colliding (overlapping) + // if( Math.abs(dx) <= w && Math.abs(dy) <= h){ console.log('colliding'); } + // else not so skip + if( Math.abs(dx) > w || Math.abs(dy) > h) continue; + + if(Math.abs(dx) < Math.abs(dy)){ // vertical collision + makeup = (h - Math.abs(dy))/2; + if(dy > 0){ // a is upper than b + na.p.position.y += makeup; + nb.p.position.y -= makeup; + }else{ // b is upper than a + na.p.position.y -= makeup; + nb.p.position.y += makeup; + } + // bounce + // https://en.wikipedia.org/wiki/Elastic_collision#One-dimensional_Newtonian + newVelY1 = (ma-mb)/(ma+mb)*na.p.velocity.y+2*mb/(ma+mb)*nb.p.velocity.y; + newVelY2 = (mb-ma)/(mb+ma)*nb.p.velocity.y+2*ma/(mb+ma)*na.p.velocity.y; + + na.p.velocity.y = newVelY1; + nb.p.velocity.y = newVelY2; + + }else{ // horizontal collision + makeup = (w - Math.abs(dx))/2; + if(dx > 0){ // a is at left of b + na.p.position.x += makeup; + nb.p.position.x -= makeup; + }else{ // b is at left of a + na.p.position.x -= makeup; + nb.p.position.x += makeup; + } + // bounce + // https://en.wikipedia.org/wiki/Elastic_collision#One-dimensional_Newtonian + newVelX1 = (ma-mb)/(ma+mb)*na.p.velocity.x+2*mb/(ma+mb)*nb.p.velocity.x; + newVelX2 = (mb-ma)/(mb+ma)*nb.p.velocity.x+2*ma/(mb+ma)*na.p.velocity.x; + + na.p.velocity.x = newVelX1; + nb.p.velocity.x = newVelX2; + } + + // slow down particule on impact + // na.p.velocity.multiplyScalar(na.center && na.p.velocity.length() < 1 ? 1.1 : 0.90); + // nb.p.velocity.multiplyScalar(nb.center && nb.p.velocity.length() < 1 ? 1.1 : 0.90); + na.p.velocity.multiplyScalar(0.90); + nb.p.velocity.multiplyScalar(0.90); + + } + } + }; + // show opened audio node + function openNodeByNid(nid){ + // console.log('Corpus openNode()', nid); + closeNode(); + if(typeof _nodes_Nid_Id[nid] != 'undefined'){ + // console.log('node exists'); + _node_opened_id = _nodes_Nid_Id[nid]; + _nodes[_nodes_Nid_Id[nid]].open(); + } + } + function closeNode(){ + if(_node_opened_id != -1){ + // if(typeof _nodes[_node_opened_id] !== 'undefined'){ + _nodes[_node_opened_id].close(); + // } + _node_opened_id = -1; + } + } + function createNodesRepulsions(){ + // how to delete all these attractions before creates new once + //console.log('_nodes_centered', _nodes_centered); + purgeNodesRepulsions(); + for (var n = 0; n < _nodes_centered.length; n++) { + for (var q = n+1; q < _nodes_centered.length; q++) { + _physics.makeAttraction(_nodes_centered[n].p, _nodes_centered[q].p, -0.4, 10); + } + } + }; + function purgeNodesRepulsions(){ + // console.log('_physics.attractions.length', _physics.attractions.length); + for (var a = _physics.attractions.length-1; a >= 0; a--) { + // assuming that only nodes repulsions have constant < 1 + // and are all at the end of array + if(Math.abs(_physics.attractions[a].constant) < 1){ + _physics.attractions.splice(a,1); + }else{ + break; + } + } + // console.log('_physics.attractions.length', _physics.attractions.length); + }; + + // ___ _ + // | __|_ _| |_ _ _ ___ ___ ___ + // | _|| ' \ _| '_/ -_) -_|_-< + // |___|_||_\__|_| \___\___/__/ + function toggleEntree(tid, bubbling){ + var $link = _$entrees_block_termlinks.filter('[tid="'+tid+'"]'); + var $li = $link.parents('li'); + var sys_path = $link.attr('data-drupal-link-system-path'); + var url = $link.attr('href'); + if(!$li.is('.opened')){ + _$entrees_block_termlinks.parents('li').removeClass('opened'); + $li.addClass('opened'); + filterEntree(tid); + if(bubbling){ + _$body.trigger({'type':'open_entree', 'tid':tid, 'url':url, 'sys_path':sys_path}); + } + // bubbling is true only when event is a real click on entree link + }else if(bubbling){ + $li.removeClass('opened'); + scrambleCollection(); + _$body.trigger({'type':'close_entree', 'tid':tid}); + } + }; + function filterEntree(t){ + shutDownArticles(); + + _nodes_centered = []; + for (var n = 0; n < _nodes.length; n++) { + if(_nodes[n].entrees.indexOf(t) == -1){ + _nodes[n].setAside(); + }else{ + _nodes[n].setCenteredOnEntree(t); + // record centered nodes for inter node repulsions + _nodes_centered.push(_nodes[n]); + } + } + createNodesRepulsions(); + }; + function highlightEntries(){ + _$entrees_block_termlinks.removeClass('highlighted'); + var id = -1; + if(_node_hover_id != -1){ + id = _node_hover_id; + }else if(_node_opened_id != -1){ + id = _node_opened_id; + } + var entree; + if(id != -1){ + for (var i = 0; i < _nodes[id].entrees.length; i++) { + entree = _nodes[id].entrees[i]; + _$entrees_block_termlinks.filter(function(){ + return $(this).attr('tid') == entree; + }).addClass('highlighted'); + } + } + }; + + // ___ _ + // / __| ___ __ _ _ _ __| |_ + // \__ \/ -_) _` | '_/ _| ' \ + // |___/\___\__,_|_| \__|_||_| + function filterSearchResults(nids){ + shutDownArticles(); + + _nodes_centered = []; + for (var n = 0; n < _nodes.length; n++) { + if(nids.indexOf(_nodes[n].nid) == -1){ + _nodes[n].setAside(); + }else{ + _nodes[n].setCentered(); + // record centered nodes for inter node repulsions + _nodes_centered.push(_nodes[n]); + } + } + createNodesRepulsions(); + }; + + // ___ _ _ _ + // / __| __ _ _ __ _ _ __ | |__| (_)_ _ __ _ + // \__ \/ _| '_/ _` | ' \| '_ \ | | ' \/ _` | + // |___/\__|_| \__,_|_|_|_|_.__/_|_|_||_\__, | + // |___/ + function scrambleCollection(){ + // console.log('scrambleCollection'); + for (var i = 0; i < _nodes.length; i++) { + _nodes[i].scramble(); + } + // setTimeout(stopScrambling, 2000); + // stopScrambling(); + }; + + function closeAllEntries(){ + _$entrees_block_termlinks.each(function(index, el) { + if($(this).parents('li').is('.opened')){ + $(this).trigger('click'); + return false; + } + }); + }; + + // _ _ _ _ + // /_\ _ _| |_(_)__| |___ ___ + // / _ \| '_| _| / _| / -_|_-< + // /_/ \_\_| \__|_\__|_\___/__/ + function initArtilesLink(){ + // add "articles link to blockentrees" + _$articles_link = $('
    ') + .html('Articles') + .attr("href", "#articles") + .addClass('articles-link') + .on('click', onCLickedOnArticles); + + $('.item-list ul',_$entrees_block).append( + $('
  • ').append( + $('').append(_$articles_link) + ) + ); + }; + function onCLickedOnArticles(e){ + e.preventDefault(); + $(this).toggleClass('is-active'); + if($(this).is('.is-active')){ + filterArticles(); + closeAllEntries(); + }else{ + resetArticlesFilter(); + } + // _$canvas.trigger({ + // 'type':'corpus-cliqued-on-articles' + // }); + return false; + }; + function filterArticles(){ + //console.log('filterArticles'); + for (var i = 0; i < _no_articles_nodes.length; i++) { + _no_articles_nodes[i].fade(); + } + }; + function resetArticlesFilter(){ + //console.log('resetArticlesFilter'); + for (var i = 0; i < _no_articles_nodes.length; i++) { + _no_articles_nodes[i].unFade(); + } + }; + function shutDownArticles(){ + // shutdown articles if active + if(_$articles_link.is('.is-active')) + _$articles_link.trigger('click'); + }; + + // ___ _ + // | __|_ _____ _ _| |_ ___ + // | _|\ V / -_) ' \ _(_-< + // |___|\_/\___|_||_\__/__/ + function initEvents(){ + console.log('Corpus, initEvents'); + _$canvas + .on('mousemove', function(event) { + event.preventDefault(); + _m_pos.x = event.originalEvent.clientX; + _m_pos.y = event.originalEvent.clientY; + }) + .on('mouseenter', function(event){ + // console.log('onMouseIN'); + _mouse_in = true; + }) + .on('mouseout', function(event){ + // console.log('onMouseOUT'); + _mouse_in = false; + _node_pop_up.removeNode(); + }) + .on('click', function(event) { + if(event.target.tagName != "A" && event.target.tagName != "INPUT"){ + // console.log("Corpus : click"); + event.preventDefault(); + if(_node_hover_id != -1){ + // console.log("corpus : click on node", _nodes[_node_hover_id]); + var event = { + 'type':'corpus-cliked-on-node', + 'target_node':{ + 'id':_node_hover_id, + 'nid':_nodes[_node_hover_id].nid, + 'audio_url':_nodes[_node_hover_id].audio_url + }, + }; + _$canvas.trigger(event); + // instead of directly opening the doc, create an event listener (e.g. : audio played from random) + // openNode(_node_hover_id); + }else{ + // console.log('corpus : click on map'); + _$canvas.trigger('corpus-cliked-on-map'); + } + } + }) + .on('audio-node-opened', function(e){ + // console.log('Corpus audio-node-opened', e); + openNodeByNid(e.nid); + }) + .on('audio-node-closed', function(e){ + closeNode(); + }) + .on('open-entree', function(e){ + toggleEntree(e.tid); + }) + .on('close-all-entree', function(e){ + closeAllEntries(); + }); + + _$entrees_block_termlinks.on('click', function(event) { + // console.log('_$entrees_block_termlinks click', this); + event.preventDefault(); + toggleEntree($(this).attr('tid'), true); + // var $link = $(this); + // var tid = $link.attr('tid'); + // var $li = $link.parents('li'); + // var sys_path = $link.attr('data-drupal-link-system-path'); + // var url = $link.attr('href'); + // if(!$li.is('.opened')){ + // _$entrees_block_termlinks.parents('li').removeClass('opened'); + // $li.addClass('opened'); + // toggleEntree(tid); + // _$body.trigger({'type':'open_entree', 'tid':tid, 'url':url, 'sys_path':sys_path}); + // }else{ + // $li.removeClass('opened'); + // scrambleCollection(); + // _$body.trigger({'type':'close_entree', 'tid':tid}); + // } + return false; + }); + + _$body + .on('chutier-action-done', function(e) { + _nodes_by_nid[e.target_id].chutier_action = e.new_action; + }) + .on('search-results-loaded', function(e){ + //console.log("Edlp Corpus, search-results-loaded",e.results); + // TODO: filter map's nodes + filterSearchResults(e.results); + }) + .on('search-closed', function(e){ + scrambleCollection(); + }) + .on('new-content-not-entree-ajax-loaded', function(e){ + // close all entries only if entry already opened + if(_$entrees_block_termlinks.parents('li.opened').length){ + _$entrees_block_termlinks.parents('li').removeClass('opened'); + scrambleCollection(); + } + }); + }; + + function checkPreOpenedEntry(){ + _$entrees_block.find('li.entree').each(function(index, el) { + var $li = $(this); + if($('a.is-active', $li).length){ + $li.addClass('opened'); + filterEntree($li.attr('tid')); + return false; + } + }); + }; + + // _ _ _ ___ _ _ + // | \| |___ __| |___| _ \___ _ __| | | |_ __ + // | .` / _ \/ _` / -_) _/ _ \ '_ \ |_| | '_ \ + // |_|\_\___/\__,_\___|_| \___/ .__/\___/| .__/ + // |_| |_| + function initNodePopup(){ + _node_pop_up = new NodePopUp(); + }; + function NodePopUp(){ + this.visible = false; + this.node; + this.$dom = $('
    ') + .addClass('node-popup') + .attr('pos', 'top-right') + .appendTo('body'); + this.$content = $('
    ').addClass('inner').appendTo(this.$dom); + + if (typeof NodePopUp.initialized == "undefined") { + + NodePopUp.prototype.setNode = function(n){ + // console.log('NodePopUp setNode()'); + this.node = n; + // positioning NodePopUp regarding node position + this.setPositioning(); + // update NodePopUp content + this.setContent(); + }; + + NodePopUp.prototype.setPositioning = function(){ + switch(true){ + case this.node.x > this.node.wall_limits.right-350 && this.node.y < this.node.wall_limits.top+200: + this.$dom.attr('pos', 'bottom-left'); + break; + case this.node.x > this.node.wall_limits.right-350: + this.$dom.attr('pos', 'top-left'); + break; + case this.node.y < this.node.wall_limits.top+200: + this.$dom.attr('pos', 'bottom-right'); + break; + default: + this.$dom.attr('pos', 'top-right'); + } + }; + + NodePopUp.prototype.setContent = function(){ + // console.log(this.node); + this.$content.html(''); + var $entrees = $('
    ').addClass('entrees'); + for (var i = 0; i < this.node.entrees.length; i++) { + var tid = this.node.entrees[i]; + $entrees.append($('').addClass('entree').attr('tid', tid)); + } + var $chutier_action = $('').addClass('chutier-icon').attr('action', this.node.chutier_action); + this.$content + .append($entrees) + .append('

    '+this.node.title+'

    ') + .append('
    '+this.node.description+'
    ') + .append($chutier_action); + }; + + NodePopUp.prototype.removeNode = function(){ + // console.log('NodePopUp removeNode()'); + this.node = false; + }; + + NodePopUp.prototype.draw = function(){ + if(this.node){ + this.$dom.css({ + 'display':"block", + 'left':this.node.x+"px", + 'top':this.node.y+"px", + }); + }else{ + this.$dom.css({ + 'display':"none", + }); + } + }; + + NodePopUp.initialized = true; + } + } + + // ___ _ + // | _ \___ _ _ __| |___ _ _ + // | / -_) ' \/ _` / -_) '_| + // |_|_\___|_||_\__,_\___|_| + function render(){ + _ctx.clearRect(0, 0, _canvas.width, _canvas.height); + + checkParticulesCollisions(); + + for (var i = 0; i < _nodes.length; i++) { + _nodes[i].onUpdate(); + } + + _node_pop_up.draw(); + + if(_node_hover_id != -1){ + _canvas.style.cursor = 'pointer'; + }else{ + _canvas.style.cursor = 'auto'; + } + // check for highlighted entries + highlightEntries(); + }; + + // ___ _ _ _ _ + // / __| |_ __ _ _ _| |_ /_\ _ _ (_)_ __ ___ + // \__ \ _/ _` | '_| _|/ _ \| ' \| | ' \/ -_) + // |___/\__\__,_|_| \__/_/ \_\_||_|_|_|_|_\___| + function startAnime(){ + _physics.onUpdate(render); + _physics.play() + $('body') + .attr('corpus', 'map-ready') + .trigger({ + 'type':'corpus-map-ready', + 'playlist':_playlist + }); + }; + + init(); + } + + $(document).ready(function($) { + var edlpcorpus = new EdlpCorpus(); + }); + +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/physics.min.js b/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/physics.min.js index 8e7487968..c3ddff68c 100644 --- a/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/physics.min.js +++ b/sites/all/modules/figli/edlp_corpus/assets/dist/scripts/physics.min.js @@ -1,38 +1,1153 @@ +/** + * Physics + * A requirified port of Traer Physics from Processing to JavaScript. + * Copyright (C) 2012 jonobr1 + * + * 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 3 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, see . + */ +require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;othis.originalPositions.length){this.originalPositions.push(new Vector());this.originalVelocities.push(new Vector());this.k1Forces.push(new Vector());this.k1Velocities.push(new Vector());this.k2Forces.push(new Vector());this.k2Velocities.push(new Vector());this.k3Forces.push(new Vector());this.k3Velocities.push(new Vector());this.k4Forces.push(new Vector());this.k4Velocities.push(new Vector());} -return this;},step:function(dt){var s=this.s;var p,x,y;this.allocateParticles();_.each(s.particles,function(p,i){if(!p.fixed){this.originalPositions[i].copy(p.position);this.originalVelocities[i].copy(p.velocity);} -p.force.clear();},this);s.applyForces();_.each(s.particles,function(p,i){if(!p.fixed){this.k1Forces[i].copy(p.force);this.k1Velocities[i].copy(p.velocity);} -p.force.clear();},this);_.each(s.particles,function(p,i){if(!p.fixed){var op=this.originalPositions[i];var k1v=this.k1Velocities[i];x=op.x+k1v.x*0.5*dt;y=op.y+k1v.y*0.5*dt;p.position.set(x,y);var ov=this.originalVelocities[i];var k1f=this.k1Forces[i];x=ov.x+k1f.x*0.5*dt/p.mass;y=ov.y+k1f.y*0.5*dt/p.mass;p.velocity.set(x,y);}},this);s.applyForces();_.each(s.particles,function(p,i){if(!p.fixed){this.k2Forces[i].copy(p.force);this.k2Velocities[i].copy(p.velocity);} -p.force.clear();},this);_.each(s.particles,function(p,i){if(!p.fixed){var op=this.originalPositions[i];var k2v=this.k2Velocities[i];p.position.set(op.x+k2v.x*0.5*dt,op.y+k2v.y*0.5*dt);var ov=this.originalVelocities[i];var k2f=this.k2Forces[i];p.velocity.set(ov.x+k2f.x*0.5*dt/p.mass,ov.y+k2f.y*0.5*dt/p.mass);}},this);s.applyForces();_.each(s.particles,function(p,i){if(!p.fixed){this.k3Forces[i].copy(p.force);this.k3Velocities[i].copy(p.velocity);} -p.force.clear();},this);_.each(s.particles,function(p,i){if(!p.fixed){var op=this.originalPositions[i];var k3v=this.k3Velocities[i];p.position.set(op.x+k3v.x*dt,op.y+k3v.y*dt) -var ov=this.originalVelocities[i];var k3f=this.k3Forces[i];p.velocity.set(ov.x+k3f.x*dt/p.mass,ov.y+k3f.y*dt/p.mass);}},this);s.applyForces();_.each(s.particles,function(p,i){if(!p.fixed){this.k4Forces[i].copy(p.force);this.k4Velocities[i].copy(p.velocity);}},this);_.each(s.particles,function(p,i){p.age+=dt;if(!p.fixed){var op=this.originalPositions[i];var k1v=this.k1Velocities[i];var k2v=this.k2Velocities[i];var k3v=this.k3Velocities[i];var k4v=this.k4Velocities[i];var x=op.x+dt/6.0*(k1v.x+2.0*k2v.x+2.0*k3v.x+k4v.x);var y=op.y+dt/6.0*(k1v.y+2.0*k2v.y+2.0*k3v.y+k4v.y);p.position.set(x,y);var ov=this.originalVelocities[i];var k1f=this.k1Forces[i];var k2f=this.k2Forces[i];var k3f=this.k3Forces[i];var k4f=this.k4Forces[i];x=ov.x+dt/(6.0*p.mass)*(k1f.x+2.0*k2f.x+2.0*k3f.x+k4f.x);y=ov.y+dt/(6.0*p.mass)*(k1f.y+2.0*k2f.y+2.0*k3f.y+k4f.y);p.velocity.set(x,y);}},this);return this;}});module.exports=Integrator;},{"./common":8,"./vector":12}],3:[function(require,module,exports){var _=require('./common'),Vector=require('./vector');function Particle(mass){this.position=new Vector();this.velocity=new Vector();this.force=new Vector();this.mass=mass;this.fixed=false;this.age=0;this.dead=false;} -_.extend(Particle.prototype,{distanceTo:function(p){return this.position.distanceTo(p.position);},makeFixed:function(){this.fixed=true;this.velocity.clear();return this;},reset:function(){this.age=0;this.dead=false;this.position.clear();this.velocity.clear();this.force.clear();this.mass=1.0;return this;},resting:function(){return this.fixed||this.velocity.isZero()&&this.force.isZero();}});module.exports=Particle;},{"./common":8,"./vector":12}],4:[function(require,module,exports){var _=require('./common'),Vector=require('./Vector'),Particle=require('./Particle'),Spring=require('./Spring'),Attraction=require('./Attraction'),Integrator=require('./Integrator');function ParticleSystem(){this.__equilibriumCriteria={particles:true,springs:true,attractions:true};this.__equilibrium=false;this.__optimized=false;this.particles=[];this.springs=[];this.attractions=[];this.forces=[];this.integrator=new Integrator(this);this.hasDeadParticles=false;var args=arguments.length;if(args===1){this.gravity=new Vector(0,arguments[0]);this.drag=ParticleSystem.DEFAULT_DRAG;}else if(args===2){this.gravity=new Vector(0,arguments[0]);this.drag=arguments[1];}else if(args===3){this.gravity=new Vector(arguments[0],arguments[1]);this.drag=arguments[3];}else{this.gravity=new Vector(0,ParticleSystem.DEFAULT_GRAVITY);this.drag=ParticleSystem.DEFAULT_DRAG;}} -_.extend(ParticleSystem,{DEFAULT_GRAVITY:0,DEFAULT_DRAG:0.001,Attraction:Attraction,Integrator:Integrator,Particle:Particle,Spring:Spring,Vector:Vector});_.extend(ParticleSystem.prototype,{optimize:function(b){this.__optimized=!!b;return this;},setGravity:function(x,y){this.gravity.set(x,y);return this;},setEquilibriumCriteria:function(particles,springs,attractions){this.__equilibriumCriteria.particles=!!particles;this.__equilibriumCriteria.springs=!!springs;this.__equilibriumCriteria.attractions=!!attractions;},tick:function(){this.integrator.step(arguments.length===0?1:arguments[0]);if(this.__optimized){this.__equilibrium=!this.needsUpdate();} -return this;},needsUpdate:function(){var i=0;if(this.__equilibriumCriteria.particles){for(i=0,l=this.particles.length;i=0||!_.isFunction(func)){return this;} -this.animations.push(func);return this;},onEquilibrium:function(func){if(_.indexOf(this.equilibriumCallbacks,func)>=0||!_.isFunction(func)){return this;} -this.equilibriumCallbacks.push(func);return this;},update:function(){if(!this.__equilibrium){return this;} -this.__equilibrium=false;if(this.playing){update.call(this);} -return this;}});function update(){var _this=this;this.tick();_.each(this.animations,function(a){a();});if((this.__optimized&&!this.__equilibrium||!this.__optimized)&&this.playing){raf(function(){update.call(_this);});} -if(this.__optimized&&this.__equilibrium){_.each(this.equilibriumCallbacks,function(a){a();});}} -module.exports=Physics;},{"./ParticleSystem":4,"./common":8,"./requestAnimationFrame":11}],6:[function(require,module,exports){var _=require('./common'),Vector=require('./vector');function Spring(a,b,k,d,l){this.constant=k;this.damping=d;this.length=l;this.a=a;this.b=b;this.on=true;} -_.extend(Spring.prototype,{currentLength:function(){return this.a.position.distanceTo(this.b.position);},update:function(){var a=this.a;var b=this.b;if(!(this.on&&(!a.fixed||!b.fixed)))return this;var a2b=new Vector().sub(a.position,b.position);var d=a2b.length();if(d===0){a2b.clear();}else{a2b.divideScalar(d);} -var fspring=-1*(d-this.length)*this.constant;var va2b=new Vector().sub(a.velocity,b.velocity);var fdamping=-1*this.damping*va2b.dot(a2b);var fr=fspring+fdamping;a2b.multiplyScalar(fr);if(!a.fixed){a.force.addSelf(a2b);} -if(!b.fixed){b.force.subSelf(a2b);} -return this;},resting:function(){var a=this.a;var b=this.b;var l=this.length;return!this.on||(a.fixed&&b.fixed)||(a.fixed&&(l===0?b.position.equals(a.position):b.position.distanceTo(a.position)<=l)&&b.resting())||(b.fixed&&(l===0?a.position.equals(b.position):a.position.distanceTo(b.position)<=l)&&a.resting());}});module.exports=Spring;},{"./common":8,"./vector":12}],7:[function(require,module,exports){var _=require('./common');function Vector(x,y){this.x=x||0;this.y=y||0;} -_.extend(Vector.prototype,{set:function(x,y){this.x=x;this.y=y;return this;},copy:function(v){this.x=v.x;this.y=v.y;return this;},clear:function(){this.x=0;this.y=0;return this;},clone:function(){return new Vector(this.x,this.y);},add:function(v1,v2){this.x=v1.x+v2.x;this.y=v1.y+v2.y;return this;},addSelf:function(v){this.x+=v.x;this.y+=v.y;return this;},sub:function(v1,v2){this.x=v1.x-v2.x;this.y=v1.y-v2.y;return this;},subSelf:function(v){this.x-=v.x;this.y-=v.y;return this;},multiplySelf:function(v){this.x*=v.x;this.y*=v.y;return this;},multiplyScalar:function(s){this.x*=s;this.y*=s;return this;},multiplyScalarXY:function(sx,sy){this.x*=sx;this.y*=sy;return this;},divideScalar:function(s){if(s){this.x/=s;this.y/=s;}else{this.set(0,0);} -return this;},negate:function(){return this.multiplyScalar(-1);},dot:function(v){return this.x*v.x+this.y*v.y;},lengthSquared:function(){return this.x*this.x+this.y*this.y;},length:function(){return Math.sqrt(this.lengthSquared());},normalize:function(){return this.divideScalar(this.length());},distanceTo:function(v){return Math.sqrt(this.distanceToSquared(v));},distanceToSquared:function(v){var dx=this.x-v.x,dy=this.y-v.y;return dx*dx+dy*dy;},setLength:function(l){return this.normalize().multiplyScalar(l);},equals:function(v){return(this.distanceTo(v)<0.0001);},lerp:function(v,t){var x=(v.x-this.x)*t+this.x;var y=(v.y-this.y)*t+this.y;return this.set(x,y);},isZero:function(){return(this.length()<0.0001);}});module.exports=Vector;},{"./common":8}],8:[function(require,module,exports){var breaker={};var ArrayProto=Array.prototype;var ObjProto=Object.prototype;var hasOwnProperty=ObjProto.hasOwnProperty;var slice=ArrayProto.slice;var nativeForEach=ArrayProto.forEach;var nativeIndexOf=ArrayProto.indexOf;var toString=ObjProto.toString;var has=function(obj,key){return hasOwnProperty.call(obj,key);};var each=function(obj,iterator,context){if(obj==null)return;if(nativeForEach&&obj.forEach===nativeForEach){obj.forEach(iterator,context);}else if(obj.length===+obj.length){for(var i=0,l=obj.length;i>1;iterator(array[mid]) this.originalPositions.length) { + this.originalPositions.push(new Vector()); + this.originalVelocities.push(new Vector()); + this.k1Forces.push(new Vector()); + this.k1Velocities.push(new Vector()); + this.k2Forces.push(new Vector()); + this.k2Velocities.push(new Vector()); + this.k3Forces.push(new Vector()); + this.k3Velocities.push(new Vector()); + this.k4Forces.push(new Vector()); + this.k4Velocities.push(new Vector()); + } + + return this; + + }, + + step: function(dt) { + + var s = this.s; + var p, x, y; + + this.allocateParticles(); + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + this.originalPositions[i].copy(p.position); + this.originalVelocities[i].copy(p.velocity); + } + p.force.clear(); + }, this); + + // K1 + + s.applyForces(); + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + this.k1Forces[i].copy(p.force); + this.k1Velocities[i].copy(p.velocity); + } + p.force.clear(); + }, this); + + // K2 + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + + var op = this.originalPositions[i]; + var k1v = this.k1Velocities[i]; + x = op.x + k1v.x * 0.5 * dt; + y = op.y + k1v.y * 0.5 * dt; + p.position.set(x, y); + + var ov = this.originalVelocities[i]; + var k1f = this.k1Forces[i]; + x = ov.x + k1f.x * 0.5 * dt / p.mass; + y = ov.y + k1f.y * 0.5 * dt / p.mass; + p.velocity.set(x, y); + + } + }, this); + + s.applyForces(); + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + this.k2Forces[i].copy(p.force); + this.k2Velocities[i].copy(p.velocity); + } + p.force.clear(); + }, this); + + // K3 + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + + var op = this.originalPositions[i]; + var k2v = this.k2Velocities[i]; + p.position.set(op.x + k2v.x * 0.5 * dt, op.y + k2v.y * 0.5 * dt); + + var ov = this.originalVelocities[i]; + var k2f = this.k2Forces[i]; + p.velocity.set(ov.x + k2f.x * 0.5 * dt / p.mass, ov.y + k2f.y * 0.5 * dt / p.mass); + } + }, this); + + s.applyForces(); + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + this.k3Forces[i].copy(p.force); + this.k3Velocities[i].copy(p.velocity); + } + p.force.clear(); + }, this); + + // K4 + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + + var op = this.originalPositions[i]; + var k3v = this.k3Velocities[i]; + p.position.set(op.x + k3v.x * dt, op.y + k3v.y * dt) + + var ov = this.originalVelocities[i]; + var k3f = this.k3Forces[i]; + p.velocity.set(ov.x + k3f.x * dt / p.mass, ov.y + k3f.y * dt / p.mass); + } + }, this); + + s.applyForces(); + + _.each(s.particles, function(p, i) { + if (!p.fixed) { + this.k4Forces[i].copy(p.force); + this.k4Velocities[i].copy(p.velocity); + } + }, this); + + // TOTAL + + _.each(s.particles, function(p, i) { + + p.age += dt; + + if (!p.fixed) { + + var op = this.originalPositions[i]; + var k1v = this.k1Velocities[i]; + var k2v = this.k2Velocities[i]; + var k3v = this.k3Velocities[i]; + var k4v = this.k4Velocities[i]; + + var x = op.x + dt / 6.0 * (k1v.x + 2.0 * k2v.x + 2.0 * k3v.x + k4v.x); + var y = op.y + dt / 6.0 * (k1v.y + 2.0 * k2v.y + 2.0 * k3v.y + k4v.y); + + p.position.set(x, y); + + var ov = this.originalVelocities[i]; + var k1f = this.k1Forces[i]; + var k2f = this.k2Forces[i]; + var k3f = this.k3Forces[i]; + var k4f = this.k4Forces[i]; + + x = ov.x + dt / (6.0 * p.mass) * (k1f.x + 2.0 * k2f.x + 2.0 * k3f.x + k4f.x); + y = ov.y + dt / (6.0 * p.mass) * (k1f.y + 2.0 * k2f.y + 2.0 * k3f.y + k4f.y); + + p.velocity.set(x, y); + + } + + }, this); + + return this; + + } + + }); + + module.exports = Integrator; + +},{"./common":8,"./vector":12}],3:[function(require,module,exports){ +var _ = require('./common') +, Vector = require('./vector') +; + + function Particle(mass) { + + this.position = new Vector(); + this.velocity = new Vector(); + this.force = new Vector(); + this.mass = mass; + this.fixed = false; + this.age = 0; + this.dead = false; + + } + + _.extend(Particle.prototype, { + + /** + * Get the distance between two particles. + */ + distanceTo: function(p) { + return this.position.distanceTo(p.position); + }, + + /** + * Make the particle fixed in 2D space. + */ + makeFixed: function() { + this.fixed = true; + this.velocity.clear(); + return this; + }, + + /** + * Reset a particle. + */ + reset: function() { + + this.age = 0; + this.dead = false; + this.position.clear(); + this.velocity.clear(); + this.force.clear(); + this.mass = 1.0; + + return this; + }, + + /** + * Returns a boolean describing whether the particle is in movement. + */ + resting: function() { + return this.fixed || this.velocity.isZero() && this.force.isZero(); + } + + }); + + module.exports = Particle; + +},{"./common":8,"./vector":12}],4:[function(require,module,exports){ +var _ = require('./common') +, Vector = require('./Vector') +, Particle = require('./Particle') +, Spring = require('./Spring') +, Attraction = require('./Attraction') +, Integrator = require('./Integrator') +; + + /** + * traer.js + * A particle-based physics engine ported from Jeff Traer's Processing + * library to JavaScript. This version is intended for use with the + * HTML5 canvas element. It is dependent on Three.js' Vector2 class, + * but can be overridden with any Vector2 class with the methods included. + * + * @author Jeffrey Traer Bernstein (original Java library) + * @author Adam Saponara (JavaScript port) + * @author Jono Brandel (requirified/optimization port) + * @author David Schoonover (Node/CommonJS/Browserify port) + * + * @version 0.3 + * @date March 25, 2012 + */ + + /** + * The whole kit and kaboodle. + * + * @class + */ + function ParticleSystem() { + + this.__equilibriumCriteria = { particles: true, springs: true, attractions: true }; + this.__equilibrium = false; // are we at equilibrium? + this.__optimized = false; + + this.particles = []; + this.springs = []; + this.attractions = []; + this.forces = []; + this.integrator = new Integrator(this); + this.hasDeadParticles = false; + + var args = arguments.length; + + if (args === 1) { + this.gravity = new Vector(0, arguments[0]); + this.drag = ParticleSystem.DEFAULT_DRAG; + } else if (args === 2) { + this.gravity = new Vector(0, arguments[0]); + this.drag = arguments[1]; + } else if (args === 3) { + this.gravity = new Vector(arguments[0], arguments[1]); + this.drag = arguments[3]; + } else { + this.gravity = new Vector(0, ParticleSystem.DEFAULT_GRAVITY); + this.drag = ParticleSystem.DEFAULT_DRAG; + } + + } + + _.extend(ParticleSystem, { + + DEFAULT_GRAVITY: 0, + + DEFAULT_DRAG: 0.001, + + Attraction: Attraction, + + Integrator: Integrator, + + Particle: Particle, + + Spring: Spring, + + Vector: Vector + + }); + + _.extend(ParticleSystem.prototype, { + + /** + * Set whether to optimize the simulation. This enables the check of whether + * particles are moving. + */ + optimize: function(b) { + this.__optimized = !!b; + return this; + }, + + /** + * Set the gravity of the ParticleSystem. + */ + setGravity: function(x, y) { + this.gravity.set(x, y); + return this; + }, + + /** + * Sets the criteria for equilibrium + */ + setEquilibriumCriteria: function(particles, springs, attractions) { + this.__equilibriumCriteria.particles = !!particles; + this.__equilibriumCriteria.springs = !!springs; + this.__equilibriumCriteria.attractions = !!attractions; + }, + + /** + * Update the integrator + */ + tick: function() { + this.integrator.step(arguments.length === 0 ? 1 : arguments[0]); + if (this.__optimized) { + this.__equilibrium = !this.needsUpdate(); + } + return this; + }, + + /** + * Checks all springs and attractions to see if the contained particles are + * inert / resting and returns a boolean. + */ + needsUpdate: function() { + var i = 0; + + if(this.__equilibriumCriteria.particles) { + for (i = 0, l = this.particles.length; i < l; i++) { + if (!this.particles[i].resting()) { + return true; + } + } + } + + if(this.__equilibriumCriteria.springs) { + for (i = 0, l = this.springs.length; i < l; i++) { + if (!this.springs[i].resting()) { + return true; + } + } + } + + if(this.__equilibriumCriteria.attractions) { + for (i = 0, l = this.attractions.length; i < l; i++) { + if (!this.attractions[i].resting()) { + return true; + } + } + } + + return false; + + }, + + /** + * Add a particle to the ParticleSystem. + */ + addParticle: function(p) { + + this.particles.push(p); + return this; + + }, + + /** + * Add a spring to the ParticleSystem. + */ + addSpring: function(s) { + + this.springs.push(s); + return this; + + }, + + /** + * Add an attraction to the ParticleSystem. + */ + addAttraction: function(a) { + + this.attractions.push(a); + return this; + + }, + + /** + * Makes and then adds Particle to ParticleSystem. + */ + makeParticle: function(m, x, y) { + + var mass = _.isNumber(m) ? m : 1.0; + var x = x || 0; + var y = y || 0; + + var p = new Particle(mass); + p.position.set(x, y); + this.addParticle(p); + return p; + + }, + + /** + * Makes and then adds Spring to ParticleSystem. + */ + makeSpring: function(a, b, k, d, l) { + + var s = new Spring(a, b, k, d, l); + this.addSpring(s); + return s; + + }, + + /** + * Makes and then adds Attraction to ParticleSystem. + */ + makeAttraction: function(a, b, k, d) { + + var a = new Attraction(a, b, k, d); + this.addAttraction(a); + return a; + + }, + + /** + * Wipe the ParticleSystem clean. + */ + clear: function() { + + this.particles.length = 0; + this.springs.length = 0; + this.attractions.length = 0; + + }, + + /** + * Calculate and apply forces. + */ + applyForces: function() { + + if (!this.gravity.isZero()) { + _.each(this.particles, function(p) { + p.force.addSelf(this.gravity); + }, this); + } + + var t = new Vector(); + + _.each(this.particles, function(p) { + t.set(p.velocity.x * -1 * this.drag, p.velocity.y * -1 * this.drag); + p.force.addSelf(t); + }, this); + + _.each(this.springs, function(s) { + s.update(); + }); + + _.each(this.attractions, function(a) { + a.update(); + }); + + _.each(this.forces, function(f) { + f.update(); + }); + + return this; + + }, + + /** + * Clear all particles in the system. + */ + clearForces: function() { + _.each(this.particles, function(p) { + p.clear(); + }); + return this; + } + + }); + + module.exports = ParticleSystem; + +},{"./Attraction":1,"./Integrator":2,"./Particle":3,"./Spring":6,"./Vector":7,"./common":8}],5:[function(require,module,exports){ +var _ = require('./common') +, raf = require('./requestAnimationFrame') +, ParticleSystem = require('./ParticleSystem') +; + + var updates = []; + + /** + * Extended singleton instance of ParticleSystem with convenience methods for + * Request Animation Frame. + * @class + */ + function Physics() { + + var _this = this; + + this.playing = false; + + ParticleSystem.apply(this, arguments); + + this.animations = []; + + this.equilibriumCallbacks = []; + + update.call(this); + + } + + _.extend(Physics, ParticleSystem, { + + superclass: ParticleSystem + + }); + + _.extend(Physics.prototype, ParticleSystem.prototype, { + + /** + * Play the animation loop. Doesn't affect whether in equilibrium or not. + */ + play: function() { + + if (this.playing) { + return this; + } + + this.playing = true; + this.__equilibrium = false; + update.call(this); + + return this; + + }, + + /** + * Pause the animation loop. Doesn't affect whether in equilibrium or not. + */ + pause: function() { + + this.playing = false; + return this; + + }, + + /** + * Toggle between playing and pausing the simulation. + */ + toggle: function() { + + if (this.playing) { + this.pause(); + } else { + this.play(); + } + + return this; + + }, + + onUpdate: function(func) { + + if (_.indexOf(this.animations, func) >= 0 || !_.isFunction(func)) { + return this; + } + + this.animations.push(func); + + return this; + + }, + + onEquilibrium: function(func) { + + if (_.indexOf(this.equilibriumCallbacks, func) >= 0 || !_.isFunction(func)) { + return this; + } + + this.equilibriumCallbacks.push(func); + + return this; + + }, + + /** + * Call update after values in the system have changed and this will fire + * it's own Request Animation Frame to update until things have settled + * to equilibrium — at which point the system will stop updating. + */ + update: function() { + + if (!this.__equilibrium) { + return this; + } + + this.__equilibrium = false; + if (this.playing) { + update.call(this); + } + + return this; + + } + + }); + + function update() { + + var _this = this; + + this.tick(); + + _.each(this.animations, function(a) { + a(); + }); + + if ((this.__optimized && !this.__equilibrium || !this.__optimized) && this.playing) { + + raf(function() { + update.call(_this); + }); + + } + + if (this.__optimized && this.__equilibrium){ + + _.each(this.equilibriumCallbacks, function(a) { + a(); + }); + + } + + } + + module.exports = Physics; + + +},{"./ParticleSystem":4,"./common":8,"./requestAnimationFrame":11}],6:[function(require,module,exports){ +var _ = require('./common') +, Vector = require('./vector') +; + + function Spring(a, b, k, d, l) { + + this.constant = k; + this.damping = d; + this.length = l; + this.a = a; + this.b = b; + this.on = true; + + } + + _.extend(Spring.prototype, { + + /** + * Returns the distance between particle a and particle b + * in 2D space. + */ + currentLength: function() { + return this.a.position.distanceTo(this.b.position); + }, + + /** + * Update spring logic. + */ + update: function() { + + var a = this.a; + var b = this.b; + if (!(this.on && (!a.fixed || !b.fixed))) return this; + + var a2b = new Vector().sub(a.position, b.position); + var d = a2b.length(); + + if (d === 0) { + a2b.clear(); + } else { + a2b.divideScalar(d); // Essentially normalize + } + + var fspring = -1 * (d - this.length) * this.constant; + + var va2b = new Vector().sub(a.velocity, b.velocity); + + var fdamping = -1 * this.damping * va2b.dot(a2b); + + var fr = fspring + fdamping; + + a2b.multiplyScalar(fr); + + if (!a.fixed) { + a.force.addSelf(a2b); + } + if (!b.fixed) { + b.force.subSelf(a2b); + } + + return this; + + }, + + /** + * Returns a boolean describing whether the spring is resting or not. + * Convenient for knowing whether or not the spring needs another update + * tick. + */ + resting: function() { + + var a = this.a; + var b = this.b; + var l = this.length; + + return !this.on || (a.fixed && b.fixed) + || (a.fixed && (l === 0 ? b.position.equals(a.position) : b.position.distanceTo(a.position) <= l) && b.resting()) + || (b.fixed && (l === 0 ? a.position.equals(b.position) : a.position.distanceTo(b.position) <= l) && a.resting()); + + } + + }); + + module.exports = Spring; + + +},{"./common":8,"./vector":12}],7:[function(require,module,exports){ +/** + * @author mr.doob / http://mrdoob.com/ + * @author philogb / http://blog.thejit.org/ + * @author egraether / http://egraether.com/ + * @author zz85 / http://www.lab4games.net/zz85/blog + * @author jonobr1 / http://jonobr1.com/ + */ + +var _ = require('./common'); + + /** + * A two dimensional vector. + */ + function Vector(x, y) { + + this.x = x || 0; + this.y = y || 0; + + } + + _.extend(Vector.prototype, { + + set: function(x, y) { + this.x = x; + this.y = y; + return this; + }, + + copy: function(v) { + this.x = v.x; + this.y = v.y; + return this; + }, + + clear: function() { + this.x = 0; + this.y = 0; + return this; + }, + + clone: function() { + return new Vector(this.x, this.y); + }, + + add: function(v1, v2) { + this.x = v1.x + v2.x; + this.y = v1.y + v2.y; + return this; + }, + + addSelf: function(v) { + this.x += v.x; + this.y += v.y; + return this; + }, + + sub: function(v1, v2) { + this.x = v1.x - v2.x; + this.y = v1.y - v2.y; + return this; + }, + + subSelf: function(v) { + this.x -= v.x; + this.y -= v.y; + return this; + }, + + multiplySelf: function(v) { + this.x *= v.x; + this.y *= v.y; + return this; + }, + + multiplyScalar: function(s) { + this.x *= s; + this.y *= s; + return this; + }, + + multiplyScalarXY: function(sx,sy) { + this.x *= sx; + this.y *= sy; + return this; + }, + + divideScalar: function(s) { + if (s) { + this.x /= s; + this.y /= s; + } else { + this.set(0, 0); + } + return this; + }, + + negate: function() { + return this.multiplyScalar(-1); + }, + + dot: function(v) { + return this.x * v.x + this.y * v.y; + }, + + lengthSquared: function() { + return this.x * this.x + this.y * this.y; + }, + + length: function() { + return Math.sqrt(this.lengthSquared()); + }, + + normalize: function() { + return this.divideScalar(this.length()); + }, + + distanceTo: function(v) { + return Math.sqrt(this.distanceToSquared(v)); + }, + + distanceToSquared: function(v) { + var dx = this.x - v.x, dy = this.y - v.y; + return dx * dx + dy * dy; + }, + + setLength: function(l) { + return this.normalize().multiplyScalar(l); + }, + + equals: function(v) { + return (this.distanceTo(v) < 0.0001 /* almost same position */); + }, + + lerp: function(v, t) { + var x = (v.x - this.x) * t + this.x; + var y = (v.y - this.y) * t + this.y; + return this.set(x, y); + }, + + isZero: function() { + return (this.length() < 0.0001 /* almost zero */ ); + } + + }); + + module.exports = Vector; + +},{"./common":8}],8:[function(require,module,exports){ + + /** + * Pulled only what's needed from: + * + * Underscore.js 1.3.3 + * (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. + * http://documentcloud.github.com/underscore + */ + + var breaker = {}; + var ArrayProto = Array.prototype; + var ObjProto = Object.prototype; + var hasOwnProperty = ObjProto.hasOwnProperty; + var slice = ArrayProto.slice; + var nativeForEach = ArrayProto.forEach; + var nativeIndexOf = ArrayProto.indexOf; + var toString = ObjProto.toString; + + var has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + var each = function(obj, iterator, context) { + + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + + }; + + var identity = function(value) { + return value; + }; + + var sortedIndex = function(array, obj, iterator) { + iterator || (iterator = identity); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >> 1; + iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; + } + return low; + }; + + module.exports = { + + has: has, + + each: each, + + extend: function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + }); + return obj; + }, + + indexOf: function(array, item, isSorted) { + if (array == null) return -1; + var i, l; + if (isSorted) { + i = sortedIndex(array, item); + return array[i] === item ? i : -1; + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); + for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; + return -1; + }, + + sortedIndex: sortedIndex, + + identity: identity, + + isNumber: function(obj) { + return toString.call(obj) == '[object Number]'; + }, + + isFunction: function(obj) { + return toString.call(obj) == '[object Function]' || typeof obj == 'function'; + }, + + isUndefined: function(obj) { + return obj === void 0; + }, + + isNull: function(obj) { + return obj === null; + } + + }; + + +},{}],"H99CHA":[function(require,module,exports){ +var root = (function(){ return this; })() +, previousShortcut = root.Physics +; + +module.exports = root.Physics = require('./Physics'); + +},{"./Physics":5}],"physics":[function(require,module,exports){ +module.exports=require('H99CHA'); +},{}],11:[function(require,module,exports){ + /* + * Requirified version of Paul Irish's request animation frame. + * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + */ +module.exports = + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; + +},{}],12:[function(require,module,exports){ +module.exports=require(7) +},{"./common":8}]},{},["H99CHA"]) diff --git a/sites/all/modules/figli/edlp_corpus/assets/dist/styles/corpus.min.css b/sites/all/modules/figli/edlp_corpus/assets/dist/styles/corpus.min.css index bc8b4c016..bf4479465 100644 --- a/sites/all/modules/figli/edlp_corpus/assets/dist/styles/corpus.min.css +++ b/sites/all/modules/figli/edlp_corpus/assets/dist/styles/corpus.min.css @@ -1 +1,71 @@ -canvas#corpus-map{position:absolute;-webkit-box-sizing:border-box;box-sizing:border-box;top:0;left:0;z-index:0}canvas#corpus-map.de-activated{background-color:#f4f4f4}div.node-popup{z-index:10;position:absolute;-webkit-box-sizing:content-box;box-sizing:content-box;width:300px;min-height:30px;top:60%;left:30%;pointer-events:none}div.node-popup .inner{padding:1em;outline:red solid 1px;background-color:#fff}div.node-popup:before{content:"";position:absolute;width:60px;height:0;border-top:1px solid red}div.node-popup[pos=bottom-right]{-webkit-transform:translateY(42px) translateX(42px);transform:translateY(42px) translateX(42px)}div.node-popup[pos=bottom-right]:before{top:-1px;left:-61px;-webkit-transform-origin:bottom right;transform-origin:bottom right;-webkit-transform:rotateZ(45deg);transform:rotateZ(45deg)}div.node-popup[pos=bottom-left]{-webkit-transform:translateX(-100%) translateY(42px) translateX(-42px);transform:translateX(-100%) translateY(42px) translateX(-42px)}div.node-popup[pos=bottom-left]:before{top:calc(100% +1px);left:100%;-webkit-transform-origin:top left;transform-origin:top left;-webkit-transform:rotateZ(-45deg);transform:rotateZ(-45deg)}div.node-popup[pos=top-left]{-webkit-transform:translateY(-100%) translateX(-100%) translateY(-42px) translateX(-42px);transform:translateY(-100%) translateX(-100%) translateY(-42px) translateX(-42px)}div.node-popup[pos=top-left]:before{bottom:0;left:100%;-webkit-transform-origin:top left;transform-origin:top left;-webkit-transform:rotateZ(45deg);transform:rotateZ(45deg)}div.node-popup[pos=top-right]{-webkit-transform:translateY(-100%) translateY(-42px) translateX(42px);transform:translateY(-100%) translateY(-42px) translateX(42px)}div.node-popup[pos=top-right]:before{top:calc(100% + 1px);left:-61px;-webkit-transform-origin:top right;transform-origin:top right;-webkit-transform:rotateZ(-45deg);transform:rotateZ(-45deg)} \ No newline at end of file +canvas#corpus-map { + position: absolute; + -webkit-box-sizing: border-box; + box-sizing: border-box; + top: 0; + left: 0; + z-index: 0; } + canvas#corpus-map.de-activated { + background-color: #f4f4f4; } + +div.node-popup { + z-index: 10; + position: absolute; + -webkit-box-sizing: content-box; + box-sizing: content-box; + width: 300px; + min-height: 30px; + pointer-events: none; + top: 60%; + left: 30%; + pointer-events: none; } + div.node-popup .inner { + padding: 1em; + outline: 1px solid red; + background-color: white; } + div.node-popup:before { + content: ""; + position: absolute; + width: 60px; + height: 0; + border-top: 1px solid red; } + div.node-popup[pos="bottom-right"] { + -webkit-transform: translateY(42px) translateX(42px); + transform: translateY(42px) translateX(42px); } + div.node-popup[pos="bottom-right"]:before { + top: -1px; + left: -61px; + -webkit-transform-origin: bottom right; + transform-origin: bottom right; + -webkit-transform: rotateZ(45deg); + transform: rotateZ(45deg); } + div.node-popup[pos="bottom-left"] { + -webkit-transform: translateX(-100%) translateY(42px) translateX(-42px); + transform: translateX(-100%) translateY(42px) translateX(-42px); } + div.node-popup[pos="bottom-left"]:before { + top: calc(100% +1px); + left: 100%; + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-transform: rotateZ(-45deg); + transform: rotateZ(-45deg); } + div.node-popup[pos="top-left"] { + -webkit-transform: translateY(-100%) translateX(-100%) translateY(-42px) translateX(-42px); + transform: translateY(-100%) translateX(-100%) translateY(-42px) translateX(-42px); } + div.node-popup[pos="top-left"]:before { + bottom: 0; + left: 100%; + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-transform: rotateZ(45deg); + transform: rotateZ(45deg); } + div.node-popup[pos="top-right"] { + -webkit-transform: translateY(-100%) translateY(-42px) translateX(42px); + transform: translateY(-100%) translateY(-42px) translateX(42px); } + div.node-popup[pos="top-right"]:before { + top: calc(100% + 1px); + left: -61px; + -webkit-transform-origin: top right; + transform-origin: top right; + -webkit-transform: rotateZ(-45deg); + transform: rotateZ(-45deg); } diff --git a/sites/all/modules/figli/edlp_search/assets/dist/scripts/edlp_search.min.js b/sites/all/modules/figli/edlp_search/assets/dist/scripts/edlp_search.min.js index d620521e8..d3b931337 100644 --- a/sites/all/modules/figli/edlp_search/assets/dist/scripts/edlp_search.min.js +++ b/sites/all/modules/figli/edlp_search/assets/dist/scripts/edlp_search.min.js @@ -1,4 +1,109 @@ +(function ($, Drupal, drupalSettings) { -(function($,Drupal,drupalSettings){var settings=drupalSettings.edlp_search;var _$body=$('body');var _$container;var _$form;function init(){initEvents();initAjax();};function initEvents(){$('body').on('new-content-ajax-loaded',initAjax).on('edlp_search_search_form-col-closed',onSearchClosed);};function initAjax(){_$form=$('#edlp-search-form:not(.ajax-enabled)');if(!_$form.length)return false;_$form=$('#edlp-search-form:not(.ajax-enabled)').on('submit',onSubmitForm).addClass('ajax-enabled');_$container=_$form.parents('.row');if(!_$container.length){_$container=_$form.parent();}};function onSubmitForm(e){e.preventDefault();var args={};args.keys=$('input[type="search"]',this).val();args.entries=[];$('input[type="checkbox"]:checked','#edit-entries').each(function(index,el){args.entries.push($(this).val());});args.langues=$('input[name="langues"]',this).val();args.genres=$('input[name="genres"]',this).val();loadResults(args);return false;};function loadResults(args){_$form.addClass('ajax-loading');_$body.addClass('ajax-loading');$('[theme="edlp_search_results"]',_$container).addClass('ajax-loading');var path=window.location.origin+drupalSettings.path.baseUrl+settings.results_ajax_url;$.getJSON(path,args).done(function(data){onResultsLoaded(data);}).fail(function(jqxhr,textStatus,error){onResultsLoadFail(jqxhr,textStatus,error);});};function onResultsLoaded(data){_$form.removeClass('ajax-loading');_$body.removeClass('ajax-loading');$prev_results=$('[theme="edlp_search_results"]',_$container);if($prev_results.length){$prev_results.replaceWith(data.rendered);}else{_$container.append(data.rendered);} -_$body.trigger({'type':'search-results-loaded','results':data.results_nids});};function onResultsLoadFail(jqxhr,textStatus,error){void 0;};function onSearchClosed(e){$('div[theme="edlp_search_results"]').remove();_$body.trigger({'type':'search-closed'});} -init();})(jQuery,Drupal,drupalSettings); \ No newline at end of file + var settings = drupalSettings.edlp_search; + var _$body = $('body'); + var _$container; + var _$form; + + function init(){ + // console.log('EdlpSearch Init', settings); + initEvents(); + initAjax(); + }; + + function initEvents(){ + $('body') + .on('new-content-ajax-loaded', initAjax) + .on('edlp_search_search_form-col-closed', onSearchClosed); + }; + + function initAjax(){ + _$form = $('#edlp-search-form:not(.ajax-enabled)'); + if(!_$form.length) return false; + + //console.log('EdlpSearch initAjaxForm()'); + _$form = $('#edlp-search-form:not(.ajax-enabled)') + .on('submit', onSubmitForm) + .addClass('ajax-enabled'); + _$container = _$form.parents('.row'); + if(!_$container.length){ + _$container = _$form.parent(); + } + }; + + function onSubmitForm(e){ + e.preventDefault(); + // console.log('onSubmitForm',e); + var args = {}; + + // search for key words + args.keys = $('input[type="search"]', this).val(); + + // entries filter + args.entries = []; + $('input[type="checkbox"]:checked', '#edit-entries').each(function(index, el) { + args.entries.push($(this).val()); + }); + + // langues + args.langues = $('input[name="langues"]', this).val(); + + // genres + args.genres = $('input[name="genres"]', this).val(); + + //console.log('EdlpSearch onSubmitForm() : args',args); + + loadResults(args); + + return false; + }; + + function loadResults(args){ + //console.log('EdlpSearch loadResults() : args', args); + _$form.addClass('ajax-loading'); + _$body.addClass('ajax-loading'); + $('[theme="edlp_search_results"]', _$container).addClass('ajax-loading'); + var path = window.location.origin + drupalSettings.path.baseUrl +settings.results_ajax_url; + $.getJSON(path, args) + .done(function(data){ + onResultsLoaded(data); + }) + .fail(function(jqxhr, textStatus, error){ + onResultsLoadFail(jqxhr, textStatus, error); + }); + }; + + function onResultsLoaded(data){ + // console.log('EdlpSearch onResultsLoaded()', data); + _$form.removeClass('ajax-loading'); + _$body.removeClass('ajax-loading'); + + // insert results col + $prev_results = $('[theme="edlp_search_results"]', _$container); + if($prev_results.length){ + $prev_results.replaceWith(data.rendered); + }else{ + _$container.append(data.rendered); + } + + // trigger event + _$body.trigger({ + 'type':'search-results-loaded', + 'results':data.results_nids + }); + + }; + + function onResultsLoadFail(jqxhr, textStatus, error){ + console.warn('EdlpSearch : search results ajax load failed : '+error, jqxhr.responseText); + }; + + function onSearchClosed(e){ + // console.log('Edlp Search onSearchClosed()'); + $('div[theme="edlp_search_results"]').remove(); + // trigger event + _$body.trigger({'type':'search-closed'}); + } + + init(); +})(jQuery, Drupal, drupalSettings); diff --git a/sites/all/modules/figli/edlp_studio/assets/dist/scripts/edlp_studio.min.js b/sites/all/modules/figli/edlp_studio/assets/dist/scripts/edlp_studio.min.js index 02a7660bb..a01cc879c 100644 --- a/sites/all/modules/figli/edlp_studio/assets/dist/scripts/edlp_studio.min.js +++ b/sites/all/modules/figli/edlp_studio/assets/dist/scripts/edlp_studio.min.js @@ -1,10 +1,408 @@ +(function ($, Drupal, drupalSettings) { -(function($,Drupal,drupalSettings){var _settings=drupalSettings.edlp_studio;var $composer;function init(){initEvents();initStudio();};function initEvents(){$('body').on('new-audio-cartel-loaded',initAjaxChutierLinks).on('new-content-ajax-loaded',initStudio).on('all-modal-closed',initStudio);};function initStudio(){if($('#studio-ui').length){$composer=$('.composition_ui .composer','#studio-ui');initAjaxChutierLinks();initAjaxCompoLinks();initDragAndDropUI();$('body').trigger({'type':'studio-initialized'});}else{$('body').trigger({'type':'studio-not-active'});}} -function initAjaxChutierLinks(){$('.chutier-ajax-link:not(.ajax-enabled)').addClass('ajax-enabled').on('click',onClickChutierAjaxLink);};function onClickChutierAjaxLink(e){e.preventDefault();addRemoveToChutier($(this));return false;};function addRemoveToChutier($link){var ajax_path=$link.attr('data-drupal-link-system-path');var path=window.location.origin+drupalSettings.path.baseUrl+ajax_path;$link.addClass('ajax-loading');$.getJSON(path,{}).done(function(data){onActionToChutierDone($link,data);}).fail(function(jqxhr,textStatus,error){onErrorActionToChutier(jqxhr,textStatus,error,$link);});};function onActionToChutierDone($link,data){$('body').trigger({'type':'chutier-action-done','action_done':data.action_done,'new_action':data.action_done=='add'?'remove':'add','target_id':$link.attr('target_id'),});$link.replaceWith(data.new_link);initAjaxChutierLinks();updateStudioChutier();};function onErrorActionToChutier(jqxhr,textStatus,error,$link){void 0;};function initAjaxCompoLinks(){$('.new-composition-link:not(.ajax-enabled)').on('click',onClickNewCompoLink).addClass('ajax-enabled');$('.composition-link:not(.ajax-enabled)').on('click',onClickCompoLink).addClass('ajax-enabled');$('.delete-composition-link:not(.ajax-enabled)').on('click',onClickDeleteCompoLink).addClass('ajax-enabled');};function onClickCompoLink(e){e.preventDefault();openCompo($(this));return false;};function openCompo($link){var cid=$link.attr('cid');var ajax_path=_settings.open_compo_ajax_url+'/'+cid;var path=window.location.origin+Drupal.url(ajax_path);$link.addClass('ajax-loading');$composer.addClass('ajax-loading');$.getJSON(path,{}).done(function(data){onOpenCompoDone(data,$link);}).fail(function(jqxhr,textStatus,error){onErrorOpenCompo(jqxhr,textStatus,error,$link);});};function onOpenCompoDone(data,$link){$('.composition-link').removeClass('is-active');$link.removeClass('ajax-loading').addClass('is-active');if($('.composition',$composer).length){$('.composition',$composer).replaceWith(data.compo);}else{$('header',$composer).after(data.compo);} -$composer.removeClass('ajax-loading');initDragAndDropUI();$('body').trigger({'type':'on-studio-compo-opened'});};function onErrorOpenCompo(jqxhr,textStatus,error,$link){$link.removeClass('ajax-loading');void 0;};function onClickNewCompoLink(e){e.preventDefault();setInputForNewCompoName($(this));return false;};function setInputForNewCompoName($link){var $form=$('
    ').addClass('new-compo-form').append($('').attr('type','text').attr('placeholder','new name')).append($('