From 24ffcb956b254622987f4193cec017f549d32fae Mon Sep 17 00:00:00 2001 From: Rob Davies Date: Mon, 22 May 2017 15:12:47 +0100 Subject: [PATCH] Pathauto and dependencies --- web/modules/contrib/ctools/LICENSE.txt | 339 ++++ web/modules/contrib/ctools/composer.json | 40 + .../ctools/config/schema/ctools.schema.yml | 63 + web/modules/contrib/ctools/ctools.info.yml | 11 + web/modules/contrib/ctools/ctools.module | 64 + .../contrib/ctools/ctools.services.yml | 38 + .../config/schema/ctools_block.schema.yml | 28 + .../ctools_block/ctools_block.info.yml | 14 + .../src/Plugin/Block/EntityField.php | 376 ++++ .../src/Plugin/Deriver/EntityFieldDeriver.php | 65 + ...e.node.ctools_block_field_test.promote.yml | 23 + ...y.node.ctools_block_field_test.default.yml | 62 + ...y.node.ctools_block_field_test.default.yml | 18 + ...ay.node.ctools_block_field_test.teaser.yml | 27 + ...ield.node.ctools_block_field_test.body.yml | 23 + ...de.ctools_block_field_test.field_image.yml | 37 + .../field.storage.node.field_image.yml | 31 + .../node.type.ctools_block_field_test.yml | 18 + .../ctools_block_field_test.info.yml | 20 + .../src/Functional/EntityFieldBlockTest.php | 132 ++ .../ctools_views/ctools_views.info.yml | 15 + .../modules/ctools_views/ctools_views.module | 104 + .../ctools_views/src/Plugin/Display/Block.php | 438 +++++ .../Tests/CToolsViewsBasicViewBlockTest.php | 350 ++++ .../ctools_views_test_views.info.yml | 20 + .../views.view.ctools_views_entity_test.yml | 543 ++++++ .../views.view.ctools_views_test_view.yml | 949 +++++++++ .../ctools/src/Access/AccessInterface.php | 9 + .../ctools/src/Access/TempstoreAccess.php | 51 + .../src/Ajax/OpenModalWizardCommand.php | 23 + .../ctools/src/Annotation/Relationship.php | 54 + .../src/ConstraintConditionInterface.php | 26 + .../ctools/src/Context/AutomaticContext.php | 25 + .../src/Context/EntityLazyLoadContext.php | 66 + .../contrib/ctools/src/ContextMapper.php | 50 + .../ctools/src/ContextMapperInterface.php | 21 + .../ctools/src/ContextNotFoundException.php | 6 + .../Controller/WizardEntityFormController.php | 52 + .../src/Controller/WizardFormController.php | 82 + .../contrib/ctools/src/Event/WizardEvent.php | 41 + .../contrib/ctools/src/Form/AjaxFormTrait.php | 43 + .../ctools/src/Form/ConditionConfigure.php | 182 ++ .../ctools/src/Form/ConditionDelete.php | 210 ++ .../ctools/src/Form/ContextConfigure.php | 254 +++ .../contrib/ctools/src/Form/ContextDelete.php | 85 + .../ctools/src/Form/ManageConditions.php | 224 +++ .../contrib/ctools/src/Form/ManageContext.php | 327 ++++ .../src/Form/ManageResolverRelationships.php | 205 ++ .../ctools/src/Form/RelationshipConfigure.php | 152 ++ .../ctools/src/Form/RequiredContext.php | 212 ++ .../ctools/src/Form/RequiredContextDelete.php | 194 ++ .../Form/ResolverRelationshipConfigure.php | 182 ++ .../src/Form/ResolverRelationshipDelete.php | 150 ++ .../src/ParamConverter/TempstoreConverter.php | 169 ++ .../ctools/src/Plugin/Block/EntityView.php | 105 + .../src/Plugin/BlockPluginCollection.php | 53 + .../src/Plugin/BlockVariantInterface.php | 96 + .../ctools/src/Plugin/BlockVariantTrait.php | 130 ++ .../src/Plugin/Condition/EntityBundle.php | 160 ++ .../ctools/src/Plugin/Condition/NodeType.php | 41 + .../src/Plugin/Deriver/EntityBundle.php | 53 + .../src/Plugin/Deriver/EntityDeriverBase.php | 51 + .../src/Plugin/Deriver/EntityViewDeriver.php | 28 + .../TypedDataEntityRelationshipDeriver.php | 24 + .../TypedDataLanguageRelationshipDeriver.php | 40 + .../Deriver/TypedDataPropertyDeriverBase.php | 113 ++ .../Deriver/TypedDataRelationshipDeriver.php | 75 + .../DisplayVariant/BlockDisplayVariant.php | 226 +++ .../src/Plugin/PluginWizardInterface.php | 25 + .../TypedDataEntityRelationship.php | 36 + .../TypedDataLanguageRelationship.php | 35 + .../Relationship/TypedDataRelationship.php | 77 + .../ctools/src/Plugin/RelationshipBase.php | 10 + .../src/Plugin/RelationshipInterface.php | 27 + .../ctools/src/Plugin/RelationshipManager.php | 35 + .../Plugin/RelationshipManagerInterface.php | 10 + .../src/Plugin/VariantCollectionInterface.php | 50 + .../src/Plugin/VariantCollectionTrait.php | 67 + .../src/Plugin/VariantPluginCollection.php | 43 + .../src/Routing/Enhancer/WizardEnhancer.php | 34 + .../ctools/src/SerializableTempstore.php | 13 + .../src/SerializableTempstoreFactory.php | 27 + .../src/Testing/EntityCreationTrait.php | 55 + .../src/Tests/Wizard/CToolsWizardTest.php | 136 ++ .../contrib/ctools/src/TypedDataResolver.php | 253 +++ .../src/Wizard/EntityFormWizardBase.php | 180 ++ .../src/Wizard/EntityFormWizardInterface.php | 39 + .../ctools/src/Wizard/FormWizardBase.php | 465 +++++ .../ctools/src/Wizard/FormWizardInterface.php | 182 ++ .../ctools/src/Wizard/WizardFactory.php | 129 ++ .../src/Wizard/WizardFactoryInterface.php | 42 + .../ctools-wizard-trail-links.html.twig | 14 + .../templates/ctools-wizard-trail.html.twig | 14 + ...tools_wizard_test_config_entity.schema.yml | 18 + .../ctools_wizard_test.info.yml | 12 + .../ctools_wizard_test.links.action.yml | 6 + .../ctools_wizard_test.links.menu.yml | 7 + .../ctools_wizard_test.routing.yml | 82 + .../src/Entity/ExampleConfigEntity.php | 83 + .../src/ExampleConfigEntityInterface.php | 26 + .../src/ExampleConfigEntityListBuilder.php | 49 + .../Form/ExampleConfigEntityDeleteForm.php | 52 + .../Form/ExampleConfigEntityExistingForm.php | 37 + .../Form/ExampleConfigEntityExternalForm.php | 66 + .../Form/ExampleConfigEntityGeneralForm.php | 46 + .../src/Form/ExampleConfigEntityOneForm.php | 62 + .../src/Form/ExampleConfigEntityTwoForm.php | 47 + .../ctools_wizard_test/src/Form/OneForm.php | 71 + .../ctools_wizard_test/src/Form/TwoForm.php | 69 + .../src/Wizard/EntityAddWizardTest.php | 15 + .../src/Wizard/EntityEditWizardTest.php | 71 + .../src/Wizard/WizardTest.php | 82 + .../src/Kernel/RelationshipManagerTest.php | 63 + .../src/Kernel/RelationshipsTestBase.php | 96 + .../src/Kernel/SerializableTempstoreTest.php | 64 + .../TypedDataEntityRelationshipPluginTest.php | 46 + ...ypedDataLanguageRelationshipPluginTest.php | 36 + .../TypedDataRelationshipPluginTest.php | 103 + .../src/Kernel/TypedDataResolverTest.php | 105 + .../src/Unit/BlockDisplayVariantTest.php | 90 + .../src/Unit/BlockPluginCollectionTest.php | 81 + .../tests/src/Unit/BlockVariantTraitTest.php | 149 ++ .../tests/src/Unit/ContextMapperTest.php | 104 + .../src/Unit/VariantCollectionTraitTest.php | 209 ++ web/modules/contrib/pathauto/LICENSE.txt | 339 ++++ web/modules/contrib/pathauto/README.md | 83 + .../config/install/pathauto.settings.yml | 13 + ...stem.action.pathauto_update_alias_node.yml | 9 + ...stem.action.pathauto_update_alias_user.yml | 9 + .../config/schema/pathauto.schema.yml | 36 + .../config/schema/pathauto_pattern.schema.yml | 40 + web/modules/contrib/pathauto/pathauto.api.php | 160 ++ .../contrib/pathauto/pathauto.info.yml | 20 + web/modules/contrib/pathauto/pathauto.install | 301 +++ web/modules/contrib/pathauto/pathauto.js | 21 + .../contrib/pathauto/pathauto.libraries.yml | 9 + .../pathauto/pathauto.links.action.yml | 6 + .../contrib/pathauto/pathauto.links.task.yml | 23 + web/modules/contrib/pathauto/pathauto.module | 188 ++ .../contrib/pathauto/pathauto.permissions.yml | 6 + .../contrib/pathauto/pathauto.routing.yml | 54 + .../contrib/pathauto/pathauto.services.yml | 26 + .../contrib/pathauto/pathauto.tokens.inc | 49 + .../contrib/pathauto/src/AliasCleaner.php | 351 ++++ .../pathauto/src/AliasCleanerInterface.php | 103 + .../pathauto/src/AliasStorageHelper.php | 255 +++ .../src/AliasStorageHelperInterface.php | 117 ++ .../src/AliasTypeBatchUpdateInterface.php | 31 + .../pathauto/src/AliasTypeInterface.php | 48 + .../contrib/pathauto/src/AliasTypeManager.php | 71 + .../contrib/pathauto/src/AliasUniquifier.php | 167 ++ .../pathauto/src/AliasUniquifierInterface.php | 42 + .../pathauto/src/Annotation/AliasType.php | 37 + .../pathauto/src/Entity/PathautoPattern.php | 370 ++++ .../PathautoSettingsCacheTag.php | 54 + .../pathauto/src/Form/PathautoAdminDelete.php | 185 ++ .../src/Form/PathautoBulkUpdateForm.php | 164 ++ .../src/Form/PathautoSettingsForm.php | 266 +++ .../pathauto/src/Form/PatternDisableForm.php | 52 + .../pathauto/src/Form/PatternEditForm.php | 285 +++ .../pathauto/src/Form/PatternEnableForm.php | 52 + .../pathauto/src/MessengerInterface.php | 20 + .../pathauto/src/PathautoGenerator.php | 365 ++++ .../src/PathautoGeneratorInterface.php | 89 + .../contrib/pathauto/src/PathautoItem.php | 58 + .../pathauto/src/PathautoPatternInterface.php | 181 ++ .../src/PathautoPatternListBuilder.php | 51 + .../contrib/pathauto/src/PathautoState.php | 97 + .../contrib/pathauto/src/PathautoWidget.php | 71 + .../src/Plugin/Action/UpdateAction.php | 36 + .../Plugin/Deriver/EntityAliasTypeDeriver.php | 94 + .../src/Plugin/pathauto/AliasType/Broken.php | 24 + .../AliasType/EntityAliasTypeBase.php | 342 ++++ .../pathauto/AliasType/ForumAliasType.php | 106 + .../src/Tests/PathautoBulkUpdateTest.php | 156 ++ .../Tests/PathautoEnablingEntityTypesTest.php | 87 + .../pathauto/src/Tests/PathautoLocaleTest.php | 201 ++ .../src/Tests/PathautoMassDeleteTest.php | 200 ++ .../src/Tests/PathautoNodeWebTest.php | 292 +++ .../src/Tests/PathautoSettingsFormWebTest.php | 240 +++ .../src/Tests/PathautoTaxonomyWebTest.php | 102 + .../src/Tests/PathautoTestHelperTrait.php | 190 ++ .../pathauto/src/Tests/PathautoUiTest.php | 189 ++ .../src/Tests/PathautoUserWebTest.php | 92 + .../contrib/pathauto/src/VerboseMessenger.php | 63 + .../config/install/views.view.articles.yml | 231 +++ .../pathauto_views_test.info.yml | 14 + .../tests/src/Kernel/PathautoKernelTest.php | 572 ++++++ .../tests/src/Kernel/PathautoTokenTest.php | 78 + .../tests/src/Unit/VerboseMessengerTest.php | 59 + web/modules/contrib/token/.travis.yml | 96 + web/modules/contrib/token/LICENSE.txt | 339 ++++ web/modules/contrib/token/README.md | 33 + .../contrib/token/css/jquery.treetable.css | 28 + web/modules/contrib/token/css/token.css | 32 + .../token/css/token.treetable.theme.css | 50 + .../contrib/token/js/jquery.treetable.js | 629 ++++++ web/modules/contrib/token/js/token.js | 58 + .../TokenAutocompleteController.php | 78 + .../src/Controller/TokenCacheController.php | 21 + .../src/Controller/TokenDevelController.php | 109 ++ .../src/Controller/TokenTreeController.php | 56 + .../token/src/Element/TokenTreeTable.php | 155 ++ .../src/Plugin/Derivative/DevelLocalTask.php | 56 + .../token/src/Routing/RouteSubscriber.php | 74 + .../token/src/Tests/TokenBlockTest.php | 72 + .../token/src/Tests/TokenCurrentPageTest.php | 66 + .../token/src/Tests/TokenFieldUiTest.php | 280 +++ .../contrib/token/src/Tests/TokenMenuTest.php | 459 +++++ .../contrib/token/src/Tests/TokenTestBase.php | 21 + .../token/src/Tests/TokenTestTrait.php | 111 ++ .../contrib/token/src/Tests/TokenURLTest.php | 51 + .../contrib/token/src/Tests/TokenUserTest.php | 116 ++ .../token/src/Tests/Tree/AutocompleteTest.php | 61 + .../token/src/Tests/Tree/HelpPageTest.php | 69 + .../src/Tests/Tree/TokenTreeTestTrait.php | 131 ++ .../contrib/token/src/Tests/Tree/TreeTest.php | 148 ++ web/modules/contrib/token/src/Token.php | 213 ++ .../contrib/token/src/TokenEntityMapper.php | 82 + .../token/src/TokenEntityMapperInterface.php | 53 + .../contrib/token/src/TokenInterface.php | 75 + .../token/src/TokenServiceProvider.php | 20 + web/modules/contrib/token/src/TreeBuilder.php | 267 +++ .../token/src/TreeBuilderInterface.php | 79 + .../token/templates/token-tree-link.html.twig | 19 + .../core.date_format.token_module_test.yml | 6 + .../Controller/TokenTreeBrowseController.php | 37 + .../token_module_test.info.yml | 12 + .../token_module_test.module | 32 + .../token_module_test.routing.yml | 7 + .../token_module_test.tokens.inc | 13 + ....entity_form_display.user.user.default.yml | 23 + .../field.field.user.user.user_picture.yml | 31 + .../field.storage.user.user_picture.yml | 25 + .../token_user_picture.info.yml | 14 + .../token/tests/src/Kernel/ArrayTest.php | 60 + .../token/tests/src/Kernel/BookTest.php | 107 + .../token/tests/src/Kernel/CommentTest.php | 102 + .../token/tests/src/Kernel/DateTest.php | 35 + .../token/tests/src/Kernel/EntityTest.php | 104 + .../token/tests/src/Kernel/FieldTest.php | 708 +++++++ .../token/tests/src/Kernel/FileTest.php | 55 + .../token/tests/src/Kernel/KernelTestBase.php | 33 + .../token/tests/src/Kernel/NodeTest.php | 99 + .../token/tests/src/Kernel/RandomTest.php | 27 + .../token/tests/src/Kernel/TaxonomyTest.php | 151 ++ .../token/tests/src/Kernel/UnitTest.php | 119 ++ web/modules/contrib/token/token.drush.inc | 20 + web/modules/contrib/token/token.info.yml | 10 + web/modules/contrib/token/token.install | 316 +++ web/modules/contrib/token/token.libraries.yml | 25 + .../contrib/token/token.links.task.yml | 3 + web/modules/contrib/token/token.module | 788 ++++++++ web/modules/contrib/token/token.pages.inc | 61 + web/modules/contrib/token/token.routing.yml | 22 + web/modules/contrib/token/token.services.yml | 12 + web/modules/contrib/token/token.tokens.inc | 1721 +++++++++++++++++ 257 files changed, 29510 insertions(+) create mode 100644 web/modules/contrib/ctools/LICENSE.txt create mode 100644 web/modules/contrib/ctools/composer.json create mode 100644 web/modules/contrib/ctools/config/schema/ctools.schema.yml create mode 100644 web/modules/contrib/ctools/ctools.info.yml create mode 100644 web/modules/contrib/ctools/ctools.module create mode 100644 web/modules/contrib/ctools/ctools.services.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/config/schema/ctools_block.schema.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php create mode 100644 web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.base_field_override.node.ctools_block_field_test.promote.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_form_display.node.ctools_block_field_test.default.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.default.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.teaser.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.body.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.field_image.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.storage.node.field_image.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/node.type.ctools_block_field_test.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php create mode 100644 web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_views/ctools_views.module create mode 100644 web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php create mode 100644 web/modules/contrib/ctools/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php create mode 100644 web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml create mode 100644 web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml create mode 100644 web/modules/contrib/ctools/src/Access/AccessInterface.php create mode 100644 web/modules/contrib/ctools/src/Access/TempstoreAccess.php create mode 100644 web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php create mode 100644 web/modules/contrib/ctools/src/Annotation/Relationship.php create mode 100644 web/modules/contrib/ctools/src/ConstraintConditionInterface.php create mode 100644 web/modules/contrib/ctools/src/Context/AutomaticContext.php create mode 100644 web/modules/contrib/ctools/src/Context/EntityLazyLoadContext.php create mode 100644 web/modules/contrib/ctools/src/ContextMapper.php create mode 100644 web/modules/contrib/ctools/src/ContextMapperInterface.php create mode 100644 web/modules/contrib/ctools/src/ContextNotFoundException.php create mode 100644 web/modules/contrib/ctools/src/Controller/WizardEntityFormController.php create mode 100644 web/modules/contrib/ctools/src/Controller/WizardFormController.php create mode 100644 web/modules/contrib/ctools/src/Event/WizardEvent.php create mode 100644 web/modules/contrib/ctools/src/Form/AjaxFormTrait.php create mode 100644 web/modules/contrib/ctools/src/Form/ConditionConfigure.php create mode 100644 web/modules/contrib/ctools/src/Form/ConditionDelete.php create mode 100644 web/modules/contrib/ctools/src/Form/ContextConfigure.php create mode 100644 web/modules/contrib/ctools/src/Form/ContextDelete.php create mode 100644 web/modules/contrib/ctools/src/Form/ManageConditions.php create mode 100644 web/modules/contrib/ctools/src/Form/ManageContext.php create mode 100644 web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php create mode 100644 web/modules/contrib/ctools/src/Form/RelationshipConfigure.php create mode 100644 web/modules/contrib/ctools/src/Form/RequiredContext.php create mode 100644 web/modules/contrib/ctools/src/Form/RequiredContextDelete.php create mode 100644 web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php create mode 100644 web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php create mode 100644 web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Block/EntityView.php create mode 100644 web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php create mode 100644 web/modules/contrib/ctools/src/Plugin/BlockVariantInterface.php create mode 100644 web/modules/contrib/ctools/src/Plugin/BlockVariantTrait.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Condition/EntityBundle.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Deriver/EntityDeriverBase.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Deriver/EntityViewDeriver.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php create mode 100644 web/modules/contrib/ctools/src/Plugin/DisplayVariant/BlockDisplayVariant.php create mode 100644 web/modules/contrib/ctools/src/Plugin/PluginWizardInterface.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataEntityRelationship.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataLanguageRelationship.php create mode 100644 web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php create mode 100644 web/modules/contrib/ctools/src/Plugin/RelationshipBase.php create mode 100644 web/modules/contrib/ctools/src/Plugin/RelationshipInterface.php create mode 100644 web/modules/contrib/ctools/src/Plugin/RelationshipManager.php create mode 100644 web/modules/contrib/ctools/src/Plugin/RelationshipManagerInterface.php create mode 100644 web/modules/contrib/ctools/src/Plugin/VariantCollectionInterface.php create mode 100644 web/modules/contrib/ctools/src/Plugin/VariantCollectionTrait.php create mode 100644 web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php create mode 100644 web/modules/contrib/ctools/src/Routing/Enhancer/WizardEnhancer.php create mode 100644 web/modules/contrib/ctools/src/SerializableTempstore.php create mode 100644 web/modules/contrib/ctools/src/SerializableTempstoreFactory.php create mode 100644 web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php create mode 100644 web/modules/contrib/ctools/src/Tests/Wizard/CToolsWizardTest.php create mode 100644 web/modules/contrib/ctools/src/TypedDataResolver.php create mode 100644 web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php create mode 100644 web/modules/contrib/ctools/src/Wizard/EntityFormWizardInterface.php create mode 100644 web/modules/contrib/ctools/src/Wizard/FormWizardBase.php create mode 100644 web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php create mode 100644 web/modules/contrib/ctools/src/Wizard/WizardFactory.php create mode 100644 web/modules/contrib/ctools/src/Wizard/WizardFactoryInterface.php create mode 100644 web/modules/contrib/ctools/templates/ctools-wizard-trail-links.html.twig create mode 100644 web/modules/contrib/ctools/templates/ctools-wizard-trail.html.twig create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/config/schema/ctools_wizard_test_config_entity.schema.yml create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.action.yml create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.menu.yml create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.routing.yml create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Entity/ExampleConfigEntity.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/ExampleConfigEntityInterface.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/ExampleConfigEntityListBuilder.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityDeleteForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExistingForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/OneForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/TwoForm.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityEditWizardTest.php create mode 100644 web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php create mode 100644 web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Kernel/TypedDataLanguageRelationshipPluginTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Unit/BlockPluginCollectionTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Unit/ContextMapperTest.php create mode 100644 web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php create mode 100644 web/modules/contrib/pathauto/LICENSE.txt create mode 100644 web/modules/contrib/pathauto/README.md create mode 100644 web/modules/contrib/pathauto/config/install/pathauto.settings.yml create mode 100644 web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_node.yml create mode 100644 web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_user.yml create mode 100644 web/modules/contrib/pathauto/config/schema/pathauto.schema.yml create mode 100644 web/modules/contrib/pathauto/config/schema/pathauto_pattern.schema.yml create mode 100644 web/modules/contrib/pathauto/pathauto.api.php create mode 100644 web/modules/contrib/pathauto/pathauto.info.yml create mode 100644 web/modules/contrib/pathauto/pathauto.install create mode 100644 web/modules/contrib/pathauto/pathauto.js create mode 100644 web/modules/contrib/pathauto/pathauto.libraries.yml create mode 100644 web/modules/contrib/pathauto/pathauto.links.action.yml create mode 100644 web/modules/contrib/pathauto/pathauto.links.task.yml create mode 100644 web/modules/contrib/pathauto/pathauto.module create mode 100644 web/modules/contrib/pathauto/pathauto.permissions.yml create mode 100644 web/modules/contrib/pathauto/pathauto.routing.yml create mode 100644 web/modules/contrib/pathauto/pathauto.services.yml create mode 100644 web/modules/contrib/pathauto/pathauto.tokens.inc create mode 100644 web/modules/contrib/pathauto/src/AliasCleaner.php create mode 100644 web/modules/contrib/pathauto/src/AliasCleanerInterface.php create mode 100644 web/modules/contrib/pathauto/src/AliasStorageHelper.php create mode 100644 web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php create mode 100644 web/modules/contrib/pathauto/src/AliasTypeBatchUpdateInterface.php create mode 100644 web/modules/contrib/pathauto/src/AliasTypeInterface.php create mode 100644 web/modules/contrib/pathauto/src/AliasTypeManager.php create mode 100644 web/modules/contrib/pathauto/src/AliasUniquifier.php create mode 100644 web/modules/contrib/pathauto/src/AliasUniquifierInterface.php create mode 100644 web/modules/contrib/pathauto/src/Annotation/AliasType.php create mode 100644 web/modules/contrib/pathauto/src/Entity/PathautoPattern.php create mode 100644 web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php create mode 100644 web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php create mode 100644 web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php create mode 100644 web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php create mode 100644 web/modules/contrib/pathauto/src/Form/PatternDisableForm.php create mode 100644 web/modules/contrib/pathauto/src/Form/PatternEditForm.php create mode 100644 web/modules/contrib/pathauto/src/Form/PatternEnableForm.php create mode 100644 web/modules/contrib/pathauto/src/MessengerInterface.php create mode 100644 web/modules/contrib/pathauto/src/PathautoGenerator.php create mode 100644 web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php create mode 100644 web/modules/contrib/pathauto/src/PathautoItem.php create mode 100644 web/modules/contrib/pathauto/src/PathautoPatternInterface.php create mode 100644 web/modules/contrib/pathauto/src/PathautoPatternListBuilder.php create mode 100644 web/modules/contrib/pathauto/src/PathautoState.php create mode 100644 web/modules/contrib/pathauto/src/PathautoWidget.php create mode 100644 web/modules/contrib/pathauto/src/Plugin/Action/UpdateAction.php create mode 100644 web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php create mode 100644 web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/Broken.php create mode 100644 web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php create mode 100644 web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoLocaleTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php create mode 100644 web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php create mode 100644 web/modules/contrib/pathauto/src/VerboseMessenger.php create mode 100644 web/modules/contrib/pathauto/tests/modules/pathauto_views_test/config/install/views.view.articles.yml create mode 100644 web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml create mode 100644 web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php create mode 100644 web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php create mode 100644 web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php create mode 100644 web/modules/contrib/token/.travis.yml create mode 100644 web/modules/contrib/token/LICENSE.txt create mode 100644 web/modules/contrib/token/README.md create mode 100644 web/modules/contrib/token/css/jquery.treetable.css create mode 100644 web/modules/contrib/token/css/token.css create mode 100644 web/modules/contrib/token/css/token.treetable.theme.css create mode 100644 web/modules/contrib/token/js/jquery.treetable.js create mode 100644 web/modules/contrib/token/js/token.js create mode 100644 web/modules/contrib/token/src/Controller/TokenAutocompleteController.php create mode 100644 web/modules/contrib/token/src/Controller/TokenCacheController.php create mode 100644 web/modules/contrib/token/src/Controller/TokenDevelController.php create mode 100644 web/modules/contrib/token/src/Controller/TokenTreeController.php create mode 100644 web/modules/contrib/token/src/Element/TokenTreeTable.php create mode 100644 web/modules/contrib/token/src/Plugin/Derivative/DevelLocalTask.php create mode 100644 web/modules/contrib/token/src/Routing/RouteSubscriber.php create mode 100644 web/modules/contrib/token/src/Tests/TokenBlockTest.php create mode 100644 web/modules/contrib/token/src/Tests/TokenCurrentPageTest.php create mode 100644 web/modules/contrib/token/src/Tests/TokenFieldUiTest.php create mode 100644 web/modules/contrib/token/src/Tests/TokenMenuTest.php create mode 100644 web/modules/contrib/token/src/Tests/TokenTestBase.php create mode 100644 web/modules/contrib/token/src/Tests/TokenTestTrait.php create mode 100644 web/modules/contrib/token/src/Tests/TokenURLTest.php create mode 100644 web/modules/contrib/token/src/Tests/TokenUserTest.php create mode 100644 web/modules/contrib/token/src/Tests/Tree/AutocompleteTest.php create mode 100644 web/modules/contrib/token/src/Tests/Tree/HelpPageTest.php create mode 100644 web/modules/contrib/token/src/Tests/Tree/TokenTreeTestTrait.php create mode 100644 web/modules/contrib/token/src/Tests/Tree/TreeTest.php create mode 100644 web/modules/contrib/token/src/Token.php create mode 100644 web/modules/contrib/token/src/TokenEntityMapper.php create mode 100644 web/modules/contrib/token/src/TokenEntityMapperInterface.php create mode 100644 web/modules/contrib/token/src/TokenInterface.php create mode 100644 web/modules/contrib/token/src/TokenServiceProvider.php create mode 100644 web/modules/contrib/token/src/TreeBuilder.php create mode 100644 web/modules/contrib/token/src/TreeBuilderInterface.php create mode 100644 web/modules/contrib/token/templates/token-tree-link.html.twig create mode 100644 web/modules/contrib/token/tests/modules/token_module_test/config/install/core.date_format.token_module_test.yml create mode 100644 web/modules/contrib/token/tests/modules/token_module_test/src/Controller/TokenTreeBrowseController.php create mode 100644 web/modules/contrib/token/tests/modules/token_module_test/token_module_test.info.yml create mode 100644 web/modules/contrib/token/tests/modules/token_module_test/token_module_test.module create mode 100644 web/modules/contrib/token/tests/modules/token_module_test/token_module_test.routing.yml create mode 100644 web/modules/contrib/token/tests/modules/token_module_test/token_module_test.tokens.inc create mode 100644 web/modules/contrib/token/tests/modules/token_user_picture/config/install/core.entity_form_display.user.user.default.yml create mode 100644 web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.field.user.user.user_picture.yml create mode 100644 web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.storage.user.user_picture.yml create mode 100644 web/modules/contrib/token/tests/modules/token_user_picture/token_user_picture.info.yml create mode 100644 web/modules/contrib/token/tests/src/Kernel/ArrayTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/BookTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/CommentTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/DateTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/EntityTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/FieldTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/FileTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/KernelTestBase.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/NodeTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/RandomTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/TaxonomyTest.php create mode 100644 web/modules/contrib/token/tests/src/Kernel/UnitTest.php create mode 100644 web/modules/contrib/token/token.drush.inc create mode 100644 web/modules/contrib/token/token.info.yml create mode 100644 web/modules/contrib/token/token.install create mode 100644 web/modules/contrib/token/token.libraries.yml create mode 100644 web/modules/contrib/token/token.links.task.yml create mode 100644 web/modules/contrib/token/token.module create mode 100644 web/modules/contrib/token/token.pages.inc create mode 100644 web/modules/contrib/token/token.routing.yml create mode 100644 web/modules/contrib/token/token.services.yml create mode 100644 web/modules/contrib/token/token.tokens.inc diff --git a/web/modules/contrib/ctools/LICENSE.txt b/web/modules/contrib/ctools/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/web/modules/contrib/ctools/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/web/modules/contrib/ctools/composer.json b/web/modules/contrib/ctools/composer.json new file mode 100644 index 000000000..62fe080cb --- /dev/null +++ b/web/modules/contrib/ctools/composer.json @@ -0,0 +1,40 @@ +{ + "name": "drupal/ctools", + "description": "Provides a number of utility and helper APIs for Drupal developers and site builders.", + "type": "drupal-module", + "license": "GPL-2.0+", + "minimum-stability": "dev", + "homepage": "https://www.drupal.org/project/ctools", + "authors": [ + { + "name": "Kris Vanderwater (EclipseGc)", + "homepage": "https://www.drupal.org/u/eclipsegc", + "role": "Maintainer" + }, + { + "name": "Jakob Perry (japerry)", + "homepage": "https://www.drupal.org/u/japerry", + "role": "Maintainer" + }, + { + "name": "Tim Plunkett (tim.plunkett)", + "homepage": "https://www.drupal.org/u/timplunkett", + "role": "Maintainer" + }, + { + "name": "James Gilliland (neclimdul)", + "homepage": "https://www.drupal.org/u/neclimdul", + "role": "Maintainer" + }, + { + "name": "Daniel Wehner (dawehner)", + "homepage": "https://www.drupal.org/u/dawehner", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/ctools", + "source": "http://cgit.drupalcode.org/ctools" + }, + "require": {} +} diff --git a/web/modules/contrib/ctools/config/schema/ctools.schema.yml b/web/modules/contrib/ctools/config/schema/ctools.schema.yml new file mode 100644 index 000000000..596c3fd68 --- /dev/null +++ b/web/modules/contrib/ctools/config/schema/ctools.schema.yml @@ -0,0 +1,63 @@ +ctools.context: + type: mapping + label: Context + mapping: + label: + type: label + label: 'Label of the context' + type: + type: string + label: 'Context type' + description: + type: string + label: 'Description of the context' + value: + type: string + label: 'Context value' + +ctools.relationship: + type: mapping + label: 'Relationship' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + +condition.plugin.entity_bundle:*: + type: condition.plugin + mapping: + bundles: + type: sequence + sequence: + type: string + +ctools.block_plugin.*: + type: block.settings.[id] + mapping: + region: + type: string + label: 'Region' + weight: + type: integer + label: 'Weight' + uuid: + type: string + label: 'UUID' + context_mapping: + type: sequence + label: 'Context assignments' + sequence: + - type: string + +ctools.block_display_variant: + type: display_variant.plugin + label: 'Block display variant' + mapping: + blocks: + type: sequence + label: 'Blocks' + sequence: + - type: ctools.block_plugin.[id] diff --git a/web/modules/contrib/ctools/ctools.info.yml b/web/modules/contrib/ctools/ctools.info.yml new file mode 100644 index 000000000..3b54b6ab9 --- /dev/null +++ b/web/modules/contrib/ctools/ctools.info.yml @@ -0,0 +1,11 @@ +name: Chaos tools +type: module +description: 'Provides a number of utility and helper APIs for Drupal developers and site builders.' +package: Chaos tool suite +# core: 8.x + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-3.0' +core: '8.x' +project: 'ctools' +datestamp: 1493401747 diff --git a/web/modules/contrib/ctools/ctools.module b/web/modules/contrib/ctools/ctools.module new file mode 100644 index 000000000..ec4daba98 --- /dev/null +++ b/web/modules/contrib/ctools/ctools.module @@ -0,0 +1,64 @@ + [ + 'variables' => [ + 'wizard' => NULL, + 'cached_values' => [], + 'trail' => [], + 'divider' => ' » ', + 'step' => NULL, + ], + ], + 'ctools_wizard_trail_links' => [ + 'variables' => [ + 'wizard' => NULL, + 'cached_values' => [], + 'trail' => [], + 'divider' => ' » ', + 'step' => NULL, + ], + ], + ]; +} + +function template_preprocess_ctools_wizard_trail(&$variables) { + /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */ + $wizard = $variables['wizard']; + $cached_values = $variables['cached_values']; + $trail = $variables['trail']; + $variables['step'] = $wizard->getStep($cached_values); + foreach ($wizard->getOperations($cached_values) as $step => $operation) { + $trail[$step] = !empty($operation['title']) ? $operation['title'] : ''; + } + $variables['trail'] = $trail; +} + +function template_preprocess_ctools_wizard_trail_links(&$variables) { + /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */ + $wizard = $variables['wizard']; + $cached_values = $variables['cached_values']; + $trail = $variables['trail']; + $variables['step'] = $wizard->getStep($cached_values); + foreach ($wizard->getOperations($cached_values) as $step => $operation) { + $parameters = $wizard->getNextParameters($cached_values); + // Override step to be the step we want. + $parameters['step'] = $step; + $trail[$step] = [ + 'title' => !empty($operation['title']) ? $operation['title'] : '', + 'url' => new \Drupal\Core\Url($wizard->getRouteName(), $parameters), + ]; + } + $variables['trail'] = $trail; +} + +function ctools_condition_info_alter(&$definitions) { + // If the node_type's class is unaltered, use a custom ctools implementation. + if (isset($definitions['node_type']) && $definitions['node_type']['class'] == 'Drupal\node\Plugin\Condition\NodeType') { + $definitions['node_type']['class'] = 'Drupal\ctools\Plugin\Condition\NodeType'; + } +} diff --git a/web/modules/contrib/ctools/ctools.services.yml b/web/modules/contrib/ctools/ctools.services.yml new file mode 100644 index 000000000..eaa38b81e --- /dev/null +++ b/web/modules/contrib/ctools/ctools.services.yml @@ -0,0 +1,38 @@ +services: + ctools.wizard.form: + class: Drupal\ctools\Controller\WizardFormController + arguments: ['@controller_resolver', '@form_builder', '@ctools.wizard.factory'] + ctools.wizard.entity.form: + class: Drupal\ctools\Controller\WizardEntityFormController + arguments: ['@controller_resolver', '@form_builder', '@ctools.wizard.factory', '@entity.manager'] + ctools.wizard_enhancer: + class: Drupal\ctools\Routing\Enhancer\WizardEnhancer + tags: + - { name: route_enhancer } + ctools.wizard.factory: + class: Drupal\ctools\Wizard\WizardFactory + arguments: ['@form_builder', '@event_dispatcher'] + ctools.paramconverter.tempstore: + class: Drupal\ctools\ParamConverter\TempstoreConverter + arguments: ['@user.shared_tempstore', '@entity_type.manager'] + tags: + - { name: paramconverter } + ctools.typed_data.resolver: + class: Drupal\ctools\TypedDataResolver + arguments: ['@typed_data_manager', '@string_translation'] + ctools.access: + class: Drupal\ctools\Access\TempstoreAccess + arguments: ['@user.shared_tempstore'] + tags: + - { name: access_check, applies_to: _ctools_access } + plugin.manager.ctools.relationship: + class: Drupal\ctools\Plugin\RelationshipManager + parent: default_plugin_manager + ctools.context_mapper: + class: Drupal\ctools\ContextMapper + arguments: ['@entity.repository'] + ctools.serializable.tempstore.factory: + class: Drupal\ctools\SerializableTempstoreFactory + arguments: ['@keyvalue.expirable', '@lock', '@request_stack', '%user.tempstore.expire%'] + tags: + - { name: backend_overridable } diff --git a/web/modules/contrib/ctools/modules/ctools_block/config/schema/ctools_block.schema.yml b/web/modules/contrib/ctools/modules/ctools_block/config/schema/ctools_block.schema.yml new file mode 100644 index 000000000..6b1520aae --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/config/schema/ctools_block.schema.yml @@ -0,0 +1,28 @@ +block.settings.entity_field:*:*: + type: block_settings + label: 'Entity field block' + mapping: + formatter: + type: mapping + label: 'Field formatter' + mapping: + type: + type: string + label: 'Format type machine name' + weight: + type: integer + label: 'Weight' + region: + type: string + label: 'Region' + label: + type: string + label: 'Label setting machine name' + settings: + type: field.formatter.settings.[%parent.type] + label: 'Settings' + third_party_settings: + type: sequence + label: 'Third party settings' + sequence: + type: field.formatter.third_party.[%key] diff --git a/web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml b/web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml new file mode 100644 index 000000000..e9fdf7c74 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/ctools_block.info.yml @@ -0,0 +1,14 @@ +name: Chaos tools blocks +type: module +description: 'Provides improvements to blocks that will one day be added to Drupal core.' +package: Chaos tool suite (Experimental) +# version: 3.x +# core: 8.x +dependencies: + - ctools + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-3.0' +core: '8.x' +project: 'ctools' +datestamp: 1493401747 diff --git a/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php b/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php new file mode 100644 index 000000000..14cf6ac21 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Block/EntityField.php @@ -0,0 +1,376 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->fieldTypeManager = $field_type_manager; + $this->formatterManager = $formatter_manager; + + // Get the entity type and field name from the plugin id. + list (, $entity_type_id, $field_name) = explode(':', $plugin_id); + $this->entityTypeId = $entity_type_id; + $this->fieldName = $field_name; + + 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('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function build() { + /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */ + $entity = $this->getContextValue('entity'); + $build = []; + /** @var \Drupal\Core\Field\FieldItemListInterface $field */ + $field = $entity->{$this->fieldName}; + $display_settings = $this->getConfiguration()['formatter']; + $build['field'] = $field->view($display_settings); + + // Set the cache data appropriately. + $build['#cache']['contexts'] = $this->getCacheContexts(); + $build['#cache']['tags'] = $this->getCacheTags(); + $build['#cache']['max-age'] = $this->getCacheMaxAge(); + + return $build; + } + + /** + * {@inheritdoc} + */ + protected function blockAccess(AccountInterface $account) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->getContextValue('entity'); + // Make sure we have access to the entity. + $access = $entity->access('view', $account, TRUE); + if ($access->isAllowed()) { + // Check that the entity in question has this field. + if ($entity instanceof FieldableEntityInterface && $entity->hasField($this->fieldName)) { + // Check field access. + $field_access = $this->entityTypeManager + ->getAccessControlHandler($this->entityTypeId) + ->fieldAccess('view', $this->getFieldDefinition(), $account); + + if ($field_access) { + // Build a renderable array for the field. + $build = $entity->get($this->fieldName)->view($this->configuration['formatter']); + // If there are actual renderable children, grant access. + if (Element::children($build)) { + return AccessResult::allowed(); + } + } + } + // Entity doesn't have this field, so access is denied. + return AccessResult::forbidden(); + } + // If we don't have access to the entity, return the forbidden result. + return $access; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $field_type_definition = $this->getFieldTypeDefinition(); + return [ + 'formatter' => [ + 'label' => 'above', + 'type' => $field_type_definition['default_formatter'] ?: '', + 'settings' => [], + 'third_party_settings' => [], + 'weight' => 0, + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $config = $this->getConfiguration(); + + $form['formatter_label'] = [ + '#type' => 'select', + '#title' => $this->t('Label'), + '#options' => [ + 'above' => $this->t('Above'), + 'inline' => $this->t('Inline'), + 'hidden' => '- ' . $this->t('Hidden') . ' -', + 'visually_hidden' => '- ' . $this->t('Visually Hidden') . ' -', + ], + '#default_value' => $config['formatter']['label'], + ]; + + $form['formatter_type'] = [ + '#type' => 'select', + '#title' => $this->t('Formatter'), + '#options' => $this->getFormatterOptions(), + '#default_value' => $config['formatter']['type'], + '#ajax' => [ + 'callback' => [static::class, 'formatterSettingsAjaxCallback'], + 'wrapper' => 'formatter-settings-wrapper', + 'effect' => 'fade', + ], + ]; + + // Add the formatter settings to the form via AJAX. + $form['#process'][] = [$this, 'formatterSettingsProcessCallback']; + $form['formatter_settings_wrapper'] = [ + '#prefix' => '
', + '#suffix' => '
', + ]; + $form['formatter_settings_wrapper']['formatter_settings'] = [ + '#tree' => TRUE, + // The settings from the formatter plugin will be added in the + // ::formatterSettingsProcessCallback method. + ]; + + return $form; + } + + /** + * Render API callback: builds the formatter settings elements. + */ + public function formatterSettingsProcessCallback(array &$element, FormStateInterface $form_state, array &$complete_form) { + $config = $this->getConfiguration(); + $parents_base = $element['#parents']; + $formatter_parent = array_merge($parents_base, ['formatter_type']); + $formatter_settings_parent = array_merge($parents_base, ['formatter_settings']); + + $settings_element = &$element['formatter_settings_wrapper']['formatter_settings']; + + // Set the #parents on the formatter_settings so they end up as a peer to + // formatter_type. + $settings_element['#parents'] = $formatter_settings_parent; + + // Get the formatter name in a way that works regardless of whether we're + // getting the value via AJAX or not. + $formatter_name = NestedArray::getValue($form_state->getUserInput(), $formatter_parent) ?: $element['formatter_type']['#default_value']; + + // Place the formatter settings on the form if a formatter is selected. + $formatter = $this->getFormatter($formatter_name, $form_state->getValue('formatter_label'), $form_state->getValue($formatter_settings_parent, $config['formatter']['settings']), $config['formatter']['third_party_settings']); + $settings_element = array_merge($formatter->settingsForm($settings_element, $form_state), $settings_element); + + // Store the array parents for our element so that we can use it to pull out + // the formatter settings in our AJAX callback. + $complete_form['#formatter_array_parents'] = $element['#array_parents']; + + return $element; + } + + /** + * Render API callback: gets the layout settings elements. + */ + public static function formatterSettingsAjaxCallback(array $form, FormStateInterface $form_state) { + $formatter_array_parents = $form['#formatter_array_parents']; + return NestedArray::getValue($form, array_merge($formatter_array_parents, ['formatter_settings_wrapper'])); + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['formatter']['label'] = $form_state->getValue('formatter_label'); + $this->configuration['formatter']['type'] = $form_state->getValue('formatter_type'); + // @todo Remove this manual cast after https://www.drupal.org/node/2635236 + // is resolved. + $this->configuration['formatter']['settings'] = (array) $form_state->getValue('formatter_settings'); + } + + /** + * Gets the field definition. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface + */ + protected function getFieldDefinition() { + if (empty($this->fieldDefinition)) { + $field_map = $this->entityFieldManager->getFieldMap(); + $bundle = reset($field_map[$this->entityTypeId][$this->fieldName]['bundles']); + $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $bundle); + $this->fieldDefinition = $field_definitions[$this->fieldName]; + } + return $this->fieldDefinition; + } + + /** + * Gets the field storage definition. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface + */ + protected function getFieldStorageDefinition() { + if (empty($this->fieldStorageDefinition)) { + $field_definitions = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId); + $this->fieldStorageDefinition = $field_definitions[$this->fieldName]; + } + return $this->fieldStorageDefinition; + } + + /** + * Gets field type definition. + * + * @return array + * The field type definition. + */ + protected function getFieldTypeDefinition() { + return $this->fieldTypeManager->getDefinition($this->getFieldStorageDefinition()->getType()); + } + + /** + * Gets the formatter options for this field type. + * + * @return array + * The formatter options. + */ + protected function getFormatterOptions() { + return $this->formatterManager->getOptions($this->getFieldStorageDefinition()->getType()); + } + + /** + * Gets the formatter object. + * + * @param string $type + * The formatter name. + * @param string $label + * The label option for the formatter. + * @param array $settings + * The formatter settings. + * @param array $third_party_settings + * The formatter third party settings. + * + * @return \Drupal\Core\Field\FormatterInterface + * The formatter object. + */ + protected function getFormatter($type, $label, array $settings, array $third_party_settings) { + return $this->formatterManager->createInstance($type, [ + 'field_definition' => $this->getFieldDefinition(), + 'view_mode' => 'default', + 'prepare' => TRUE, + 'label' => $label, + 'settings' => $settings, + 'third_party_settings' => $third_party_settings, + ]); + } + + public function __wakeup() { + parent::__wakeup(); + // @todo figure out why this happens. + // prevent $fieldStorageDefinition being erroneously set to $this. + $this->fieldStorageDefinition = NULL; + } + + +} diff --git a/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php b/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php new file mode 100644 index 000000000..ac5b5d176 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/src/Plugin/Deriver/EntityFieldDeriver.php @@ -0,0 +1,65 @@ +entityManager->getEntityTypeLabels(); + foreach ($this->entityManager->getFieldMap() as $entity_type_id => $entity_field_map) { + foreach ($this->entityManager->getFieldStorageDefinitions($entity_type_id) as $field_storage_definition) { + $field_name = $field_storage_definition->getName(); + + // The blocks are based on fields. However, we are looping through field + // storages for which no fields may exist. If that is the case, skip + // this field storage. + if (!isset($entity_field_map[$field_name])) { + continue; + } + + $field_info = $entity_field_map[$field_name]; + $derivative_id = $entity_type_id . ":" . $field_name; + + // Get the admin label for both base and configurable fields. + if ($field_storage_definition->isBaseField()) { + $admin_label = $field_storage_definition->getLabel(); + } + else { + // We take the field label used on the first bundle. + $first_bundle = reset($field_info['bundles']); + $bundle_field_definitions = $this->entityManager->getFieldDefinitions($entity_type_id, $first_bundle); + + // The field storage config may exist, but it's possible that no + // fields are actually using it. If that's the case, skip to the next + // field. + if (empty($bundle_field_definitions[$field_name])) { + continue; + } + $admin_label = $bundle_field_definitions[$field_name]->getLabel(); + } + + // Set plugin definition for derivative. + $derivative = $base_plugin_definition; + $derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]); + $derivative['admin_label'] = $admin_label; + $derivative['context'] = [ + 'entity' => new ContextDefinition('entity:' . $entity_type_id, $entity_type_labels[$entity_type_id], TRUE), + ]; + + $this->derivatives[$derivative_id] = $derivative; + + } + } + return $this->derivatives; + } + +} diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.base_field_override.node.ctools_block_field_test.promote.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.base_field_override.node.ctools_block_field_test.promote.yml new file mode 100644 index 000000000..6b1a61005 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.base_field_override.node.ctools_block_field_test.promote.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + config: + - node.type.ctools_block_field_test +_core: + default_config_hash: hoRuk0InNhhRVGnhQ9hzifTXVz432i9hvPe-tVstUbc +id: node.ctools_block_field_test.promote +field_name: promote +entity_type: node +bundle: ctools_block_field_test +label: 'Promoted to front page' +description: '' +required: false +translatable: true +default_value: + - + value: 0 +default_value_callback: '' +settings: + on_label: 'On' + off_label: 'Off' +field_type: boolean diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_form_display.node.ctools_block_field_test.default.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_form_display.node.ctools_block_field_test.default.yml new file mode 100644 index 000000000..acfaaefc4 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_form_display.node.ctools_block_field_test.default.yml @@ -0,0 +1,62 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.ctools_block_field_test.body + - node.type.ctools_block_field_test + module: + - path + - text +_core: + default_config_hash: xpDeWNLzjX4dDB9YyBlO9y9FR4vHwMv0GKsuqDYy0Bc +id: node.ctools_block_field_test.default +targetEntityType: node +bundle: ctools_block_field_test +mode: default +content: + body: + type: text_textarea_with_summary + weight: 31 + settings: + rows: 9 + summary_rows: 3 + placeholder: '' + third_party_settings: { } + created: + type: datetime_timestamp + weight: 10 + settings: { } + third_party_settings: { } + path: + type: path + weight: 30 + settings: { } + third_party_settings: { } + promote: + type: boolean_checkbox + settings: + display_label: true + weight: 15 + third_party_settings: { } + sticky: + type: boolean_checkbox + settings: + display_label: true + weight: 16 + third_party_settings: { } + title: + type: string_textfield + weight: -5 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + third_party_settings: { } +hidden: { } diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.default.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.default.yml new file mode 100644 index 000000000..3844350c4 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.default.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.field.node.ctools_block_field_test.body + - node.type.ctools_block_field_test + module: + - user +_core: + default_config_hash: clNnyw6fhh5SwIme5I_3zjbLv-PMfpY-JXofVAC3CV8 +id: node.ctools_block_field_test.default +targetEntityType: node +bundle: ctools_block_field_test +mode: default +content: { } +hidden: + body: true + links: true diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.teaser.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.teaser.yml new file mode 100644 index 000000000..8b55c0b35 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/core.entity_view_display.node.ctools_block_field_test.teaser.yml @@ -0,0 +1,27 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.teaser + - field.field.node.ctools_block_field_test.body + - node.type.ctools_block_field_test + module: + - text + - user +_core: + default_config_hash: gQV5baI7pCIOBUtLkbJ1c2WJwM8CdlKiKOtLLIWnfy0 +id: node.ctools_block_field_test.teaser +targetEntityType: node +bundle: ctools_block_field_test +mode: teaser +content: + body: + label: hidden + type: text_summary_or_trimmed + weight: 101 + settings: + trim_length: 600 + third_party_settings: { } + links: + weight: 100 +hidden: { } diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.body.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.body.yml new file mode 100644 index 000000000..42853da52 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.body.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.body + - node.type.ctools_block_field_test + module: + - text +_core: + default_config_hash: fzUnwtwftRsgKExjpF6XdMqbUzP16ytkjQniBZl1Hqg +id: node.ctools_block_field_test.body +field_name: body +entity_type: node +bundle: ctools_block_field_test +label: Body +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + display_summary: true +field_type: text_with_summary diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.field_image.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.field_image.yml new file mode 100644 index 000000000..0696ece53 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.field.node.ctools_block_field_test.field_image.yml @@ -0,0 +1,37 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_image + - node.type.ctools_block_field_test + module: + - image +id: node.ctools_block_field_test.field_image +field_name: field_image +entity_type: node +bundle: ctools_block_field_test +label: Image +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + file_directory: '[date:custom:Y]-[date:custom:m]' + file_extensions: 'png gif jpg jpeg' + max_filesize: '' + max_resolution: '' + min_resolution: '' + alt_field: true + title_field: false + alt_field_required: true + title_field_required: false + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null + handler: 'default:file' + handler_settings: { } +field_type: image diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.storage.node.field_image.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.storage.node.field_image.yml new file mode 100644 index 000000000..e4da70854 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/field.storage.node.field_image.yml @@ -0,0 +1,31 @@ +langcode: en +status: true +dependencies: + module: + - file + - image + - node +id: node.field_image +field_name: field_image +entity_type: node +type: image +settings: + uri_scheme: public + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null + target_type: file + display_field: false + display_default: false +module: image +locked: false +cardinality: 1 +translatable: true +indexes: + target_id: + - target_id +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/node.type.ctools_block_field_test.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/node.type.ctools_block_field_test.yml new file mode 100644 index 000000000..caf9cff81 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/config/install/node.type.ctools_block_field_test.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - menu_ui +third_party_settings: + menu_ui: + available_menus: { } + parent: '' +_core: + default_config_hash: hjC271ZF6B5XYgF6-F5Ak73sJiZWJRmSLsgk8S7Vo-8 +name: 'CTools block field test' +type: ctools_block_field_test +description: 'A content type used for the ctools_block field tests.' +help: '' +new_revision: false +preview_mode: 0 +display_submitted: false diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml new file mode 100644 index 000000000..17bf6d513 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/modules/ctools_block_field_test/ctools_block_field_test.info.yml @@ -0,0 +1,20 @@ +name: 'Chaos tools blocks test' +type: module +description: 'Support module for Chaos tools blocks tests.' +# core: 8.x +package: Testing +# version: 8.0.1 +dependencies: + - image + - menu_ui + - node + - path + - text + - user +features: true + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-3.0' +core: '8.x' +project: 'ctools' +datestamp: 1493401747 diff --git a/web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php b/web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php new file mode 100644 index 000000000..4fa77c5ec --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_block/tests/src/Functional/EntityFieldBlockTest.php @@ -0,0 +1,132 @@ +drupalPlaceBlock('entity_field:node:body', [ + 'formatter' => [ + 'type' => 'text_default', + ], + 'context_mapping' => [ + 'entity' => '@node.node_route_context:node', + ], + ]); + $node = $this->drupalCreateNode(['type' => 'ctools_block_field_test']); + $this->drupalGet('node/' . $node->id()); + $assert = $this->assertSession(); + $assert->pageTextContains($block->label()); + $assert->pageTextContains($node->body->value); + + $node->set('body', NULL)->save(); + $this->getSession()->reload(); + // The block should not appear if there is no value in the field. + $assert->pageTextNotContains($block->label()); + } + + /** + * Tests that empty image fields will still render their default value. + */ + public function testEmptyImageField() { + $source = \Drupal::moduleHandler()->getModule('image')->getPath() . '/sample.png'; + file_unmanaged_copy($source, 'public://sample.png'); + + /** @var \Drupal\file\FileInterface $file */ + $file = \Drupal::entityTypeManager() + ->getStorage('file') + ->create([ + 'uri' => 'public://sample.png', + ]); + $file->save(); + + /** @var \Drupal\field\FieldConfigInterface $field */ + $field = \Drupal::entityTypeManager() + ->getStorage('field_config') + ->load('node.ctools_block_field_test.field_image'); + $settings = $field->getSettings(); + $settings['default_image']['uuid'] = $file->uuid(); + $field->set('settings', $settings)->save(); + + $this->drupalPlaceBlock('entity_field:node:field_image', [ + 'formatter' => [ + 'type' => 'image_image', + ], + 'context_mapping' => [ + 'entity' => '@node.node_route_context:node', + ], + ]); + + $node = $this->drupalCreateNode(['type' => 'ctools_block_field_test']); + $this->drupalGet('node/' . $node->id()); + + $url = $file->getFileUri(); + $url = file_create_url($url); + $url = file_url_transform_relative($url); + $this->assertSession()->responseContains('src="' . $url . '"'); + } + + /** + * Tests using the node uid base field in a block. + */ + public function testNodeBaseFields() { + $block = $this->drupalPlaceBlock('entity_field:node:title', [ + 'formatter' => [ + 'type' => 'string', + ], + 'context_mapping' => [ + 'entity' => '@node.node_route_context:node', + ], + ]); + $node = $this->drupalCreateNode(['type' => 'ctools_block_field_test', 'uid' => 1]); + $this->drupalGet('node/' . $node->id()); + $assert = $this->assertSession(); + $assert->pageTextContains($block->label()); + $assert->pageTextContains($node->getTitle()); + } + + /** + * Tests that we are setting the render cache metadata correctly. + */ + public function testRenderCache() { + $this->drupalPlaceBlock('entity_field:node:body', [ + 'formatter' => [ + 'type' => 'text_default', + ], + 'context_mapping' => [ + 'entity' => '@node.node_route_context:node', + ], + ]); + $a = $this->drupalCreateNode(['type' => 'ctools_block_field_test']); + $b = $this->drupalCreateNode(['type' => 'ctools_block_field_test']); + + $assert = $this->assertSession(); + $this->drupalGet('node/' . $a->id()); + $assert->pageTextContains($a->body->value); + $this->drupalGet('node/' . $b->id()); + $assert->pageTextNotContains($a->body->value); + $assert->pageTextContains($b->body->value); + + $text = 'This is my text. Are you not entertained?'; + $a->body->value = $text; + $a->save(); + $this->drupalGet('node/' . $a->id()); + $assert->pageTextContains($text); + } + +} diff --git a/web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml b/web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml new file mode 100644 index 000000000..2d66b5b53 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_views/ctools_views.info.yml @@ -0,0 +1,15 @@ +name: Chaos tools Views +type: module +description: 'A set of improvements to the core Views code that allows for greater control over Blocks.' +package: Chaos tool suite (Experimental) +# version: 3.x +# core: 8.x +dependencies: + - block + - views + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-3.0' +core: '8.x' +project: 'ctools' +datestamp: 1493401747 diff --git a/web/modules/contrib/ctools/modules/ctools_views/ctools_views.module b/web/modules/contrib/ctools/modules/ctools_views/ctools_views.module new file mode 100644 index 000000000..845feb6a0 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_views/ctools_views.module @@ -0,0 +1,104 @@ + 'string', + 'label' => 'Pager type' + ]; + $definitions['views_block']['mapping']['fields'] = [ + 'type' => 'sequence', + 'label' => 'Fields settings', + 'sequence' => [ + [ + 'type' => 'mapping', + 'label' => 'Field settings', + 'mapping' => [ + 'hide' => [ + 'type' => 'boolean', + 'label' => 'Hide field', + ], + 'weight' => [ + 'type' => 'integer', + 'label' => 'Field weight', + ], + ], + ], + ], + ]; + $definitions['views_block']['mapping']['filter'] = [ + 'type' => 'sequence', + 'label' => 'Filters settings', + 'sequence' => [ + [ + 'type' => 'mapping', + 'label' => 'Filter settings', + 'mapping' => [ + 'type' => [ + 'type' => 'string', + 'label' => 'Plugin id', + ], + 'disable' => [ + 'type' => 'boolean', + 'label' => 'Disable filter', + ], + ], + ], + ], + ]; + $definitions['views_block']['mapping']['sort'] = [ + 'type' => 'sequence', + 'label' => 'Sort settings', + 'sequence' => [ + [ + 'type' => 'string', + 'label' => 'Sort order value', + ], + ], + ]; + $definitions['views_block']['mapping']['pager_offset'] = [ + 'type' => 'integer', + 'label' => 'Pager offset' + ]; + + // Add to the views block display plugin schema. + $definitions['views.display.block']['mapping']['allow']['mapping']['offset'] = [ + 'type' => 'string', + 'label' => 'Pager offset', + ]; + $definitions['views.display.block']['mapping']['allow']['mapping']['pager'] = [ + 'type' => 'string', + 'label' => 'Pager type', + ]; + $definitions['views.display.block']['mapping']['allow']['mapping']['hide_fields'] = [ + 'type' => 'string', + 'label' => 'Hide fields', + ]; + $definitions['views.display.block']['mapping']['allow']['mapping']['sort_fields'] = [ + 'type' => 'string', + 'label' => 'Sort fields', + ]; + $definitions['views.display.block']['mapping']['allow']['mapping']['disable_filters'] = [ + 'type' => 'string', + 'label' => 'Disable filters', + ]; + $definitions['views.display.block']['mapping']['allow']['mapping']['configure_sorts'] = [ + 'type' => 'string', + 'label' => 'Configure sorts', + ]; +} diff --git a/web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php b/web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php new file mode 100644 index 000000000..e2d2a7c4d --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_views/src/Plugin/Display/Block.php @@ -0,0 +1,438 @@ +getOption('allow')); + $filter_options = [ + 'items_per_page' => $this->t('Items per page'), + 'offset' => $this->t('Pager offset'), + 'pager' => $this->t('Pager type'), + 'hide_fields' => $this->t('Hide fields'), + 'sort_fields' => $this->t('Reorder fields'), + 'disable_filters' => $this->t('Disable filters'), + 'configure_sorts' => $this->t('Configure sorts') + ]; + $filter_intersect = array_intersect_key($filter_options, $filtered_allow); + + $options['allow'] = array( + 'category' => 'block', + 'title' => $this->t('Allow settings'), + 'value' => empty($filtered_allow) ? $this->t('None') : implode(', ', $filter_intersect), + ); + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + $options = $form['allow']['#options']; + $options['offset'] = $this->t('Pager offset'); + $options['pager'] = $this->t('Pager type'); + $options['hide_fields'] = $this->t('Hide fields'); + $options['sort_fields'] = $this->t('Reorder fields'); + $options['disable_filters'] = $this->t('Disable filters'); + $options['configure_sorts'] = $this->t('Configure sorts'); + $form['allow']['#options'] = $options; + // Update the items_per_page if set. + $defaults = array_filter($form['allow']['#default_value']); + if (isset($defaults['items_per_page'])) { + $defaults['items_per_page'] = 'items_per_page'; + } + $form['allow']['#default_value'] = $defaults; + } + + /** + * {@inheritdoc} + */ + public function blockForm(ViewsBlock $block, array &$form, FormStateInterface $form_state) { + $form = parent::blockForm($block, $form, $form_state); + + $allow_settings = array_filter($this->getOption('allow')); + $block_configuration = $block->getConfiguration(); + + // Modify "Items per page" block settings form. + if (!empty($allow_settings['items_per_page'])) { + // Items per page + $form['override']['items_per_page']['#type'] = 'number'; + unset($form['override']['items_per_page']['#options']); + } + + // Provide "Pager offset" block settings form. + if (!empty($allow_settings['offset'])) { + $form['override']['pager_offset'] = [ + '#type' => 'number', + '#title' => $this->t('Pager offset'), + '#default_value' => isset($block_configuration['pager_offset']) ? $block_configuration['pager_offset'] : 0, + '#description' => $this->t('For example, set this to 3 and the first 3 items will not be displayed.'), + ]; + } + + // Provide "Pager type" block settings form. + if (!empty($allow_settings['pager'])) { + $pager_options = [ + 'view' => $this->t('Inherit from view'), + 'some' => $this->t('Display a specified number of items'), + 'none' => $this->t('Display all items') + ]; + $form['override']['pager'] = [ + '#type' => 'radios', + '#title' => $this->t('Pager'), + '#options' => $pager_options, + '#default_value' => isset($block_configuration['pager']) ? $block_configuration['pager'] : 'view' + ]; + } + + // Provide "Hide fields" / "Reorder fields" block settings form. + if (!empty($allow_settings['hide_fields']) || !empty($allow_settings['sort_fields'])) { + // Set up the configuration table for hiding / sorting fields. + $fields = $this->getHandlers('field'); + $header = []; + if (!empty($allow_settings['hide_fields'])) { + $header['hide'] = $this->t('Hide'); + } + $header['label'] = $this->t('Label'); + if (!empty($allow_settings['sort_fields'])) { + $header['weight'] = $this->t('Weight'); + } + $form['override']['order_fields'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => array(), + ]; + if (!empty($allow_settings['sort_fields'])) { + $form['override']['order_fields']['#tabledrag'] = [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'field-weight', + ] + ]; + $form['override']['order_fields']['#attributes'] = ['id' => 'order-fields']; + } + + // Sort available field plugins by their currently configured weight. + $sorted_fields = []; + if (!empty($allow_settings['sort_fields']) && isset($block_configuration['fields'])) { + uasort($block_configuration['fields'], '\Drupal\ctools_views\Plugin\Display\Block::sortFieldsByWeight'); + foreach (array_keys($block_configuration['fields']) as $field_name) { + if (!empty($fields[$field_name])) { + $sorted_fields[$field_name] = $fields[$field_name]; + unset($fields[$field_name]); + } + } + if (!empty($fields)) { + foreach ($fields as $field_name => $field_info) { + $sorted_fields[$field_name] = $field_info; + } + } + } + else { + $sorted_fields = $fields; + } + + // Add each field to the configuration table. + foreach ($sorted_fields as $field_name => $plugin) { + $field_label = $plugin->adminLabel(); + if (!empty($plugin->options['label'])) { + $field_label .= ' (' . $plugin->options['label'] . ')'; + } + if (!empty($allow_settings['sort_fields'])) { + $form['override']['order_fields'][$field_name]['#attributes']['class'][] = 'draggable'; + } + $form['override']['order_fields'][$field_name]['#weight'] = !empty($block_configuration['fields'][$field_name]['weight']) ? $block_configuration['fields'][$field_name]['weight'] : ''; + if (!empty($allow_settings['hide_fields'])) { + $form['override']['order_fields'][$field_name]['hide'] = [ + '#type' => 'checkbox', + '#default_value' => !empty($block_configuration['fields'][$field_name]['hide']) ? $block_configuration['fields'][$field_name]['hide'] : 0, + ]; + } + $form['override']['order_fields'][$field_name]['label'] = [ + '#markup' => $field_label, + ]; + if (!empty($allow_settings['sort_fields'])) { + $form['override']['order_fields'][$field_name]['weight'] = [ + '#type' => 'weight', + '#title' => $this->t('Weight for @title', ['@title' => $field_label]), + '#title_display' => 'invisible', + '#delta' => 50, + '#default_value' => !empty($block_configuration['fields'][$field_name]['weight']) ? $block_configuration['fields'][$field_name]['weight'] : 0, + '#attributes' => ['class' => ['field-weight']], + ]; + } + } + } + + // Provide "Configure filters" / "Disable filters" block settings form. + if (!empty($allow_settings['disable_filters'])) { + $items = []; + foreach ((array) $this->getOption('filters') as $filter_name => $item) { + $item['value'] = isset($block_configuration["filter"][$filter_name]['value']) ? $block_configuration["filter"][$filter_name]['value'] : ''; + $items[$filter_name] = $item; + } + $this->setOption('filters', $items); + $filters = $this->getHandlers('filter'); + + // Add a settings form for each exposed filter to configure or hide it. + foreach ($filters as $filter_name => $plugin) { + if ($plugin->isExposed() && $exposed_info = $plugin->exposedInfo()) { + $form['override']['filters'][$filter_name] = [ + '#type' => 'details', + '#title' => $exposed_info['label'], + ]; + $form['override']['filters'][$filter_name]['plugin'] = [ + '#type' => 'value', + '#value' => $plugin, + ]; + // Render "Disable filters" settings form. + if (!empty($allow_settings['disable_filters'])) { + $form['override']['filters'][$filter_name]['disable'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Disable'), + '#default_value' => !empty($block_configuration['filter'][$filter_name]['disable']) ? $block_configuration['filter'][$filter_name]['disable'] : 0, + ]; + } + } + } + } + + // Provide "Configure sorts" block settings form. + if (!empty($allow_settings['configure_sorts'])) { + $sorts = $this->getHandlers('sort'); + $options = array( + 'ASC' => $this->t('Sort ascending'), + 'DESC' => $this->t('Sort descending'), + ); + foreach ($sorts as $sort_name => $plugin) { + $form['override']['sort'][$sort_name] = [ + '#type' => 'details', + '#title' => $plugin->adminLabel(), + ]; + $form['override']['sort'][$sort_name]['plugin'] = [ + '#type' => 'value', + '#value' => $plugin, + ]; + $form['override']['sort'][$sort_name]['order'] = array( + '#title' => $this->t('Order'), + '#type' => 'radios', + '#options' => $options, + '#default_value' => $plugin->options['order'] + ); + + // Set default values for sorts for this block. + if (!empty($block_configuration["sort"][$sort_name])) { + $form['override']['sort'][$sort_name]['order']['#default_value'] = $block_configuration["sort"][$sort_name]; + } + } + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit(ViewsBlock $block, $form, FormStateInterface $form_state) { + // Set default value for items_per_page if left blank. + if (empty($form_state->getValue(array('override', 'items_per_page')))) { + $form_state->setValue(array('override', 'items_per_page'), "none"); + } + + parent::blockSubmit($block, $form, $form_state); + $configuration = $block->getConfiguration(); + $allow_settings = array_filter($this->getOption('allow')); + + // Save "Pager type" settings to block configuration. + if (!empty($allow_settings['pager'])) { + if ($pager = $form_state->getValue(['override', 'pager'])) { + $configuration['pager'] = $pager; + } + } + + // Save "Pager offset" settings to block configuration. + if (!empty($allow_settings['offset'])) { + $configuration['pager_offset'] = $form_state->getValue(['override', 'pager_offset']); + } + + // Save "Hide fields" / "Reorder fields" settings to block configuration. + if (!empty($allow_settings['hide_fields']) || !empty($allow_settings['sort_fields'])) { + if ($fields = array_filter($form_state->getValue(['override', 'order_fields']))) { + uasort($fields, '\Drupal\ctools_views\Plugin\Display\Block::sortFieldsByWeight'); + $configuration['fields'] = $fields; + } + } + + // Save "Configure filters" / "Disable filters" settings to block + // configuration. + unset($configuration['filter']); + if (!empty($allow_settings['disable_filters'])) { + if ($filters = $form_state->getValue(['override', 'filters'])) { + foreach ($filters as $filter_name => $filter) { + /** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $plugin */ + $plugin = $form_state->getValue(['override', 'filters', $filter_name, 'plugin']); + $configuration["filter"][$filter_name]['type'] = $plugin->getPluginId(); + + // Check if we want to disable this filter. + if (!empty($allow_settings['disable_filters'])) { + $disable = $form_state->getValue(['override', 'filters', $filter_name, 'disable']); + // If marked disabled, we don't really care about other stuff. + if ($disable) { + $configuration["filter"][$filter_name]['disable'] = $disable; + continue; + } + } + } + } + } + + // Save "Configure sorts" settings to block configuration. + if (!empty($allow_settings['configure_sorts'])) { + $sorts = $form_state->getValue(['override', 'sort']); + foreach ($sorts as $sort_name => $sort) { + $plugin = $sort['plugin']; + // Check if we want to override the default sort order + if ($plugin->options['order'] != $sort['order']) { + $configuration['sort'][$sort_name] = $sort['order']; + } + } + } + + $block->setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public function preBlockBuild(ViewsBlock $block) { + parent::preBlockBuild($block); + + $allow_settings = array_filter($this->getOption('allow')); + $config = $block->getConfiguration(); + list(, $display_id) = explode('-', $block->getDerivativeId(), 2); + + // Change pager offset settings based on block configuration. + if (!empty($allow_settings['offset'])) { + $this->view->setOffset($config['pager_offset']); + } + + // Change pager style settings based on block configuration. + if (!empty($allow_settings['pager'])) { + $pager = $this->view->display_handler->getOption('pager'); + if (!empty($config['pager']) && $config['pager'] != 'view') { + $pager['type'] = $config['pager']; + } + $this->view->display_handler->setOption('pager', $pager); + } + + // Change fields output based on block configuration. + if (!empty($allow_settings['hide_fields']) || !empty($allow_settings['sort_fields'])) { + if (!empty($config['fields']) && $this->view->getStyle()->usesFields()) { + $fields = $this->view->getHandlers('field'); + uasort($config['fields'], '\Drupal\ctools_views\Plugin\Display\Block::sortFieldsByWeight'); + $iterate_fields = !empty($allow_settings['sort_fields']) ? $config['fields'] : $fields; + foreach (array_keys($iterate_fields) as $field_name) { + // Remove each field in sequence and re-add them to sort + // appropriately or hide if disabled. + $this->view->removeHandler($display_id, 'field', $field_name); + if (empty($allow_settings['hide_fields']) || (!empty($allow_settings['hide_fields']) && empty($config['fields'][$field_name]['hide']))) { + $this->view->addHandler($display_id, 'field', $fields[$field_name]['table'], $fields[$field_name]['field'], $fields[$field_name], $field_name); + } + } + } + } + + // Change filters output based on block configuration. + if (!empty($allow_settings['disable_filters'])) { + $filters = $this->view->getHandlers('filter', $display_id); + foreach ($filters as $filter_name => $filter) { + // If we allow disabled filters and this filter is disabled, disable it + // and continue. + if (!empty($allow_settings['disable_filters']) && !empty($config["filter"][$filter_name]['disable'])) { + $this->view->removeHandler($display_id, 'filter', $filter_name); + continue; + } + } + } + + // Change sorts based on block configuration. + if (!empty($allow_settings['configure_sorts'])) { + $sorts = $this->view->getHandlers('sort', $display_id); + foreach ($sorts as $sort_name => $sort) { + if (!empty($config["sort"][$sort_name])) { + $sort['order'] = $config["sort"][$sort_name]; + $this->view->setHandler($display_id, 'sort', $sort_name, $sort); + } + } + } + } + + protected function getFilterOptionsValue(array $filter, array $config) { + $plugin_definition = \Drupal::service('plugin.manager.views.filter')->getDefinition($config['type']); + if (is_subclass_of($plugin_definition['class'], '\Drupal\views\Plugin\views\filter\InOperator')) { + return array_values($config['value']); + } + return $config['value'][$filter['expose']['identifier']]; + } + + /** + * {@inheritdoc} + */ + public function usesExposed() { + $filters = $this->getHandlers('filter'); + foreach ($filters as $filter_name => $filter) { + if ($filter->isExposed() && !empty($filter->exposedInfo())) { + return TRUE; + } + } + return FALSE; + } + + /** + * Exposed widgets typically only work with ajax in Drupal core, however + * #2605218 totally breaks the rest of the functionality in this display and + * in Core's Block display as well, so we allow non-ajax block views to use + * exposed filters and manually set the #action to the current request uri. + */ + public function elementPreRender(array $element) { + /** @var \Drupal\views\ViewExecutable $view */ + $view = $element['#view']; + if (!empty($view->exposed_widgets['#action']) && !$view->ajaxEnabled()) { + $view->exposed_widgets['#action'] = \Drupal::request()->getRequestUri(); + } + return parent::elementPreRender($element); + } + + /** + * Sort field config array by weight. + * + * @param $a + * @param $b + * @return int + */ + public static function sortFieldsByWeight($a, $b) { + $a_weight = isset($a['weight']) ? $a['weight'] : 0; + $b_weight = isset($b['weight']) ? $b['weight'] : 0; + if ($a_weight == $b_weight) { + return 0; + } + return ($a_weight < $b_weight) ? -1 : 1; + } + +} diff --git a/web/modules/contrib/ctools/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php b/web/modules/contrib/ctools/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php new file mode 100644 index 000000000..85201046a --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php @@ -0,0 +1,350 @@ +storage = $this->container->get('entity.manager')->getStorage('block'); + } + + /** + * Test ctools_views "items_per_page" configuration. + */ + public function testItemsPerPage() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme); + $this->assertFieldByXPath('//input[@type="number" and @name="settings[override][items_per_page]"]', NULL, 'items_per_page setting is a number field'); + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][items_per_page]'] = 0; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme, $edit, $this->t('Save block')); + + // Assert items per page default settings. + $this->drupalGet(''); + $result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2'); + $this->assertEqual((string) $result[0], 'CTools Views Pager Block'); + $this->assertRaw('Showing 3 records on page 1'); + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_pager")]//table/tbody/tr'))); + + // Override items per page settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][items_per_page]'] = 2; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_pager'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual(2, $config['items_per_page'], "'Items per page' is properly saved."); + + // Assert items per page overridden settings. + $this->drupalGet(''); + $result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2'); + $this->assertEqual((string) $result[0], 'CTools Views Pager Block'); + $this->assertRaw('Showing 2 records on page 1'); + $this->assertEqual(2, count($this->xpath('//div[contains(@class, "view-display-id-block_pager")]//table/tbody/tr'))); + $this->assertEqual([1, 2], $this->xpath('//div[contains(@class, "view-display-id-block_pager")]//table//tr//td[contains(@class, "views-field-id")]')); + } + + /** + * Test ctools_views "offset" configuration. + */ + public function testOffset() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme); + $this->assertFieldByXPath('//input[@type="number" and @name="settings[override][pager_offset]"]', NULL, 'items_per_page setting is a number field'); + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][items_per_page]'] = 0; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme, $edit, $this->t('Save block')); + + // Assert pager offset default settings. + $this->drupalGet(''); + $this->assertEqual([1, 2, 3], $this->xpath('//div[contains(@class, "view-display-id-block_pager")]//table//tr//td[contains(@class, "views-field-id")]')); + + // Override pager offset settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][items_per_page]'] = 0; + $edit['settings[override][pager_offset]'] = 1; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_pager'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual(1, $config['pager_offset'], "'Pager offset' is properly saved."); + + // Assert pager offset overridden settings. + $this->drupalGet(''); + $this->assertEqual([2, 3, 4], $this->xpath('//div[contains(@class, "view-display-id-block_pager")]//table//tr//td[contains(@class, "views-field-id")]')); + } + + /** + * Test ctools_views "pager" configuration. + */ + public function testPager() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme); + $this->assertFieldById('edit-settings-override-pager-view', 'view'); + $this->assertFieldById('edit-settings-override-pager-some'); + $this->assertFieldById('edit-settings-override-pager-none'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][items_per_page]'] = 0; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_pager/' . $default_theme, $edit, $this->t('Save block')); + + // Assert pager default settings. + $this->drupalGet(''); + $this->assertText('Page 1'); + $this->assertText('Next ›'); + + // Override pager settings to 'some'. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][items_per_page]'] = 0; + $edit['settings[override][pager]'] = 'some'; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_pager'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual('some', $config['pager'], "'Pager' setting is properly saved."); + + // Assert pager overridden settings to 'some', showing no pager. + $this->drupalGet(''); + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_pager")]//table/tbody/tr'))); + $this->assertNoText('Page 1'); + $this->assertNoText('Next ›'); + + // Override pager settings to 'none'. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][items_per_page]'] = 0; + $edit['settings[override][pager]'] = 'none'; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_pager', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_pager'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual('none', $config['pager'], "'Pager' setting is properly saved."); + + // Assert pager overridden settings to 'some', showing no pager. + $this->drupalGet(''); + $this->assertEqual(5, count($this->xpath('//div[contains(@class, "view-display-id-block_pager")]//table/tbody/tr'))); + $this->assertNoText('Page 1'); + $this->assertNoText('Next ›'); + } + + /** + * Test ctools_views 'hide_fields' configuration. + */ + public function testHideFields() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme); + $this->assertFieldById('edit-settings-override-order-fields-id-hide'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme, $edit, $this->t('Save block')); + + // Assert hide_fields default settings. + $this->drupalGet(''); + $this->assertEqual(5, count($this->xpath('//div[contains(@class, "view-display-id-block_fields")]//table//td[contains(@class, "views-field-id")]'))); + + // Override hide_fields settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][order_fields][id][hide]'] = 1; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_fields', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_fields'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual(1, $config['fields']['id']['hide'], "'hide_fields' setting is properly saved."); + $this->assertEqual(0, $config['fields']['name']['hide'], "'hide_fields' setting is properly saved."); + + // Assert hide_fields overridden settings. + $this->drupalGet(''); + $this->assertEqual(0, count($this->xpath('//div[contains(@class, "view-display-id-block_fields")]//table//td[contains(@class, "views-field-id")]'))); + } + + /** + * Test ctools_views 'sort_fields' configuration. + */ + public function testOrderFields() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme); + $this->assertFieldById('edit-settings-override-order-fields-id-weight', 0); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_fields/' . $default_theme, $edit, $this->t('Save block')); + + // Assert sort_fields default settings. + $this->drupalGet(''); + // Check that the td with class "views-field-id" is the first td in the first tr element. + $this->assertEqual(0, count($this->xpath('count(//div[contains(@class, "view-display-id-block_fields")]//table//tr[1]//td[contains(@class, "views-field-id")]/preceding-sibling::td)'))); + + // Override sort_fields settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][order_fields][name][weight]'] = -50; + $edit['settings[override][order_fields][age][weight]'] = -49; + $edit['settings[override][order_fields][job][weight]'] = -48; + $edit['settings[override][order_fields][created][weight]'] = -47; + $edit['settings[override][order_fields][id][weight]'] = -46; + $edit['settings[override][order_fields][name_1][weight]'] = -45; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_fields', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_fields'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual(-46, $config['fields']['id']['weight'], "'sort_fields' setting is properly saved."); + $this->assertEqual(-50, $config['fields']['name']['weight'], "'sort_fields' setting is properly saved."); + + // Assert sort_fields overridden settings. + $this->drupalGet(''); + + // Check that the td with class "views-field-id" is the 5th td in the first tr element. + $this->assertEqual(4, count($this->xpath('//div[contains(@class, "view-display-id-block_fields")]//table//tr[1]//td[contains(@class, "views-field-id")]/preceding-sibling::td'))); + + // Check that duplicate fields in the View produce expected outpu + $name1_element = $this->xpath('//div[contains(@class, "view-display-id-block_fields")]//table//tr[1]/td[contains(@class, "views-field-name")]/text()'); + $name1 = (string) $name1_element[0]; + $this->assertEqual("John", trim($name1)); + $name2_element = $this->xpath('//div[contains(@class, "view-display-id-block_fields")]//table//tr[1]/td[contains(@class, "views-field-name-1")]/text()'); + $name2 = (string) $name2_element[0]; + $this->assertEqual("John", trim($name2)); + } + + /** + * Test ctools_views 'disable_filters' configuration. + */ + public function testDisableFilters() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme); + $this->assertFieldById('edit-settings-override-filters-status-disable'); + $this->assertFieldById('edit-settings-override-filters-job-disable'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme, $edit, $this->t('Save block')); + + // Assert disable_filters default settings. + $this->drupalGet(''); + // Check that the default settings show both filters + $this->assertFieldByXPath('//select[@name="status"]'); + $this->assertFieldByXPath('//input[@name="job"]'); + + // Override disable_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][status][disable]'] = 1; + $edit['settings[override][filters][job][disable]'] = 1; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_filter', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_filter'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual(1, $config['filter']['status']['disable'], "'disable_filters' setting is properly saved."); + $this->assertEqual(1, $config['filter']['job']['disable'], "'disable_filters' setting is properly saved."); + + // Assert disable_filters overridden settings. + $this->drupalGet(''); + $this->assertNoFieldByXPath('//select[@name="status"]'); + $this->assertNoFieldByXPath('//input[@name="job"]'); + } + + /** + * Test ctools_views 'configure_sorts' configuration. + */ + public function testConfigureSorts() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_sort/' . $default_theme); + $this->assertFieldByXPath('//input[@name="settings[override][sort][id][order]"]'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_sort/' . $default_theme, $edit, $this->t('Save block')); + + // Assert configure_sorts default settings. + $this->drupalGet(''); + // Check that the results are sorted ASC + $element = $this->xpath('//div[contains(@class, "view-display-id-block_sort")]//table//tr[1]/td[1]/text()'); + $value = (string) $element[0]; + $this->assertEqual("1", trim($value)); + + // Override configure_sorts settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][sort][id][order]'] = "DESC"; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_sort', $edit, $this->t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_sort'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual("DESC", $config['sort']['id'], "'configure_sorts' setting is properly saved."); + + // Assert configure_sorts overridden settings. + // Check that the results are sorted DESC + $this->drupalGet(''); + $element = $this->xpath('//div[contains(@class, "view-display-id-block_sort")]//table//tr[1]/td[1]/text()'); + $value = (string) $element[0]; + $this->assertEqual("5", trim($value)); + } +} diff --git a/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml b/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml new file mode 100644 index 000000000..092f98c37 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/ctools_views_test_views.info.yml @@ -0,0 +1,20 @@ +name: 'CTools Views test views' +type: module +description: 'Provides default views for CTools Views tests.' +package: Testing +# core: 8.x +dependencies: + - views + - block + - entity_test + - ctools_views + - text + - user + - node + - taxonomy + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-3.0' +core: '8.x' +project: 'ctools' +datestamp: 1493401747 diff --git a/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml b/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml new file mode 100644 index 000000000..c2ece3c05 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml @@ -0,0 +1,543 @@ +langcode: en +status: true +dependencies: + config: + - node.type.ctools_views + - taxonomy.vocabulary.tags + module: + - datetime + - node + - options + - taxonomy + - user +id: ctools_views_entity_test +label: 'CTools Views Entity Test View' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + offset: 0 + style: + type: table + row: + type: fields + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: Title + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + ctools_views: ctools_views + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: 'CTools Views Entity Test View' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } + block_filter_date: + display_plugin: block + id: block_filter_date + display_title: 'Date filter' + position: 4 + display_options: + display_extenders: { } + display_description: '' + title: 'Date filter' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + ctools_views: ctools_views + entity_type: node + entity_field: type + plugin_id: bundle + group: 1 + field_ctools_views_date_value: + id: field_ctools_views_date_value + table: node__field_ctools_views_date + field: field_ctools_views_date_value + relationship: none + group_type: group + admin_label: '' + operator: between + group: 1 + exposed: true + expose: + operator_id: field_ctools_views_date_value_op + label: 'CTools Views Date (field_ctools_views_date)' + description: '' + use_operator: false + operator: field_ctools_views_date_value_op + identifier: field_ctools_views_date_value + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator1: '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: datetime + filter_groups: + operator: AND + groups: + 1: AND + allow: + items_per_page: false + offset: '0' + pager: '0' + hide_fields: '0' + sort_fields: '0' + disable_filters: '0' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } + block_filter_list: + display_plugin: block + id: block_filter_list + display_title: 'List filter' + position: 3 + display_options: + display_extenders: { } + display_description: '' + title: 'List filter' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + ctools_views: ctools_views + entity_type: node + entity_field: type + plugin_id: bundle + field_ctools_views_list_value: + id: field_ctools_views_list_value + table: node__field_ctools_views_list + field: field_ctools_views_list_value + relationship: none + group_type: group + admin_label: '' + operator: or + value: { } + group: 1 + exposed: true + expose: + operator_id: field_ctools_views_list_value_op + label: 'Ctools Views List (field_ctools_views_list)' + description: '' + use_operator: false + operator: field_ctools_views_list_value_op + identifier: field_ctools_views_list_value + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator1: '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: { } + reduce_duplicates: false + plugin_id: list_field + filter_groups: + operator: AND + groups: + 1: AND + allow: + items_per_page: false + offset: '0' + pager: '0' + hide_fields: '0' + sort_fields: '0' + disable_filters: '0' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } + block_filter_tax: + display_plugin: block + id: block_filter_tax + display_title: 'Taxonomy filter' + position: 2 + display_options: + display_extenders: { } + display_description: '' + title: 'Taxonomy filter' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + ctools_views: ctools_views + entity_type: node + entity_field: type + plugin_id: bundle + group: 1 + field_ctools_views_tags_target_id: + id: field_ctools_views_tags_target_id + table: node__field_ctools_views_tags + field: field_ctools_views_tags_target_id + relationship: none + group_type: group + admin_label: '' + operator: or + value: { } + group: 1 + exposed: true + expose: + operator_id: field_ctools_views_tags_target_id_op + label: 'Tags (field_ctools_views_tags)' + description: '' + use_operator: false + operator: field_ctools_views_tags_target_id_op + identifier: field_ctools_views_tags_target_id + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator1: '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: { } + reduce_duplicates: false + type: select + limit: true + vid: tags + hierarchy: false + error_message: true + plugin_id: taxonomy_index_tid + filter_groups: + operator: AND + groups: + 1: AND + allow: + items_per_page: false + offset: '0' + pager: '0' + hide_fields: '0' + sort_fields: '0' + disable_filters: '0' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user + - 'user.node_grants:view' + - user.permissions + tags: { } + block_filter_text: + display_plugin: block + id: block_filter_text + display_title: 'Textfield filter' + position: 1 + display_options: + display_extenders: { } + display_description: '' + title: 'Textfield filter' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + ctools_views: ctools_views + entity_type: node + entity_field: type + plugin_id: bundle + field_ctools_views_text_value: + id: field_ctools_views_text_value + table: node__field_ctools_views_text + field: field_ctools_views_text_value + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: true + expose: + operator_id: field_ctools_views_text_value_op + label: 'Text (field_ctools_views_text)' + description: '' + use_operator: false + operator: field_ctools_views_text_value_op + identifier: field_ctools_views_text_value + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator1: '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 + filter_groups: + operator: AND + groups: + 1: AND + allow: + items_per_page: false + offset: '0' + pager: '0' + hide_fields: '0' + sort_fields: '0' + disable_filters: '0' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml b/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml new file mode 100644 index 000000000..7187b56c1 --- /dev/null +++ b/web/modules/contrib/ctools/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml @@ -0,0 +1,949 @@ +langcode: en +status: true +dependencies: { } +id: ctools_views_test_view +label: 'CTools Views Test View' +module: views +description: '' +tag: '' +base_table: views_test_data +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + offset: 0 + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + id: id + age: age + created: created + id_1: id_1 + job: job + name: name + info: + id: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + age: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + created: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + id_1: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + job: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + name: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: '-1' + empty_table: false + row: + type: fields + fields: + id: + id: id + table: views_test_data + field: id + relationship: none + group_type: group + admin_label: '' + label: ID + 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 + set_precision: false + precision: 0 + decimal: . + separator: '' + format_plural: false + format_plural_string: "1\x03@count" + prefix: '' + suffix: '' + entity_type: null + entity_field: null + plugin_id: numeric + name: + id: name + table: views_test_data + field: name + relationship: none + group_type: group + admin_label: '' + label: Name + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + plugin_id: standard + age: + id: age + table: views_test_data + field: age + relationship: none + group_type: group + admin_label: '' + label: Age + 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 + set_precision: false + precision: 0 + decimal: . + separator: '' + format_plural: false + format_plural_string: "1\x03@count" + prefix: '' + suffix: '' + plugin_id: numeric + job: + id: job + table: views_test_data + field: job + relationship: none + group_type: group + admin_label: '' + label: Job + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + plugin_id: standard + created: + id: created + table: views_test_data + 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: false + 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: '' + plugin_id: date + filters: { } + sorts: + id: + id: id + table: views_test_data + field: id + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + plugin_id: standard + title: 'CTools Views Test View' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + use_ajax: false + filter_groups: + operator: AND + groups: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + tags: { } + block_fields: + display_plugin: block + id: block_fields + display_title: 'CTools Views Fields Block' + position: 2 + display_options: + display_extenders: { } + block_category: 'CTools Views' + allow: + hide_fields: hide_fields + sort_fields: sort_fields + items_per_page: false + offset: '0' + pager: '0' + disable_filters: '0' + block_description: 'CTools Views Fields Block' + display_description: '' + pager: + type: none + options: + offset: 0 + defaults: + pager: false + title: false + fields: false + title: 'CTools Views Fields Block' + fields: + id: + id: id + table: views_test_data + field: id + relationship: none + group_type: group + admin_label: '' + label: ID + 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 + set_precision: false + precision: 0 + decimal: . + separator: '' + format_plural: false + format_plural_string: "1\x03@count" + prefix: '' + suffix: '' + entity_type: null + entity_field: null + plugin_id: numeric + name: + id: name + table: views_test_data + field: name + relationship: none + group_type: group + admin_label: '' + label: Name + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + plugin_id: standard + age: + id: age + table: views_test_data + field: age + relationship: none + group_type: group + admin_label: '' + label: Age + 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 + set_precision: false + precision: 0 + decimal: . + separator: '' + format_plural: false + format_plural_string: "1\x03@count" + prefix: '' + suffix: '' + plugin_id: numeric + job: + id: job + table: views_test_data + field: job + relationship: none + group_type: group + admin_label: '' + label: Job + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + plugin_id: standard + created: + id: created + table: views_test_data + 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: false + 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: '' + plugin_id: date + name_1: + id: name_1 + table: views_test_data + field: name + relationship: none + group_type: group + admin_label: '' + label: '2nd name field' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + plugin_id: standard + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + tags: { } + block_filter: + display_plugin: block + id: block_filter + display_title: 'CTools Views Filter Block' + position: 3 + display_options: + display_extenders: { } + display_description: '' + block_category: 'CTools Views' + block_description: 'CTools Views Filter Block' + allow: + disable_filters: disable_filters + items_per_page: false + offset: '0' + pager: '0' + hide_fields: '0' + sort_fields: '0' + filters: + status: + id: status + table: views_test_data + field: status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: true + group: 1 + exposed: true + expose: + operator_id: '' + label: Status + description: '' + use_operator: false + operator: status_op + identifier: status + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator1: '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 + job: + id: job + table: views_test_data + field: job + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: true + expose: + operator_id: job_op + label: Job + description: '' + use_operator: false + operator: job_op + identifier: job + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator1: '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 + defaults: + filters: false + filter_groups: false + title: false + filter_groups: + operator: AND + groups: + 1: AND + title: 'CTools Views Filter Block' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + - url + tags: { } + block_pager: + display_plugin: block + id: block_pager + display_title: 'CTools Views Pager Block' + position: 1 + display_options: + display_extenders: { } + block_description: 'CTools Views Pager Block' + block_category: 'CTools Views' + allow: + items_per_page: true + offset: offset + pager: pager + hide_fields: '0' + sort_fields: '0' + disable_filters: '0' + display_description: '' + header: + result: + id: result + table: views + field: result + relationship: none + group_type: group + admin_label: '' + empty: false + content: "Displaying @start - @end of @total\nShowing @current_record_count records on page @current_page" + plugin_id: result + defaults: + header: false + pager: false + title: false + pager: + type: mini + options: + items_per_page: 3 + offset: 0 + id: 0 + total_pages: null + tags: + previous: '‹ Previous' + next: 'Next ›' + 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 + title: 'CTools Views Pager Block' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + - url.query_args + tags: { } + block_sort: + display_plugin: block + id: block_sort + display_title: 'CTools Views Sort Block' + position: 4 + display_options: + display_extenders: { } + display_description: '' + title: 'CTools Views Sort Block' + defaults: + title: false + block_description: 'CTools Views Sort Block' + block_category: 'CTools Views' + allow: + configure_sorts: configure_sorts + items_per_page: false + offset: '0' + pager: '0' + hide_fields: '0' + sort_fields: '0' + disable_filters: '0' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + tags: { } diff --git a/web/modules/contrib/ctools/src/Access/AccessInterface.php b/web/modules/contrib/ctools/src/Access/AccessInterface.php new file mode 100644 index 000000000..1e12fd29c --- /dev/null +++ b/web/modules/contrib/ctools/src/Access/AccessInterface.php @@ -0,0 +1,9 @@ +tempstore = $tempstore; + } + + protected function getTempstore() { + return $this->tempstore; + } + + public function access(Route $route, RouteMatch $match, AccountInterface $account) { + $tempstore_id = $match->getParameter('tempstore_id') ? $match->getParameter('tempstore_id') : $route->getDefault('tempstore_id'); + $id = $match->getParameter($route->getRequirement('_ctools_access')); + if ($tempstore_id && $id) { + $cached_values = $this->getTempstore()->get($tempstore_id)->get($id); + if (!empty($cached_values['access']) && ($cached_values['access'] instanceof CToolsAccessInterface)) { + $access = $cached_values['access']->access($account); + } + else { + $access = AccessResult::allowed(); + } + } + else { + $access = AccessResult::forbidden(); + } + // The different wizards will have different tempstore ids and adding this + // cache context allows us to nuance the access per wizard. + $access->addCacheContexts(['url.query_args:tempstore_id']); + return $access; + } +} \ No newline at end of file diff --git a/web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php b/web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php new file mode 100644 index 000000000..fc1642cdc --- /dev/null +++ b/web/modules/contrib/ctools/src/Ajax/OpenModalWizardCommand.php @@ -0,0 +1,23 @@ + $tempstore_id, + 'machine_name' => NULL, + 'step' => NULL, + ]; + $form = \Drupal::service('ctools.wizard.factory')->getWizardForm($object, $parameters, TRUE); + $title = isset($form['#title']) ? $form['#title'] : ''; + $content = $form; + + parent::__construct($title, $content, $dialog_options, $settings); + } + +} diff --git a/web/modules/contrib/ctools/src/Annotation/Relationship.php b/web/modules/contrib/ctools/src/Annotation/Relationship.php new file mode 100644 index 000000000..452f6992b --- /dev/null +++ b/web/modules/contrib/ctools/src/Annotation/Relationship.php @@ -0,0 +1,54 @@ +entityRepository = $entity_repository; + $this->uuid = $uuid; + } + + /** + * {@inheritdoc} + */ + public function getContextValue() { + if (!$this->contextData) { + $entity_type_id = substr($this->contextDefinition->getDataType(), 7); + $this->setContextValue($this->entityRepository->loadEntityByUuid($entity_type_id, $this->uuid)); + } + return parent::getContextValue(); + } + + /** + * {@inheritdoc} + */ + public function hasContextValue() { + // Ensure that the entity is loaded before checking if it exists. + if (!$this->contextData) { + $this->getContextValue(); + } + return parent::hasContextValue(); + } + +} diff --git a/web/modules/contrib/ctools/src/ContextMapper.php b/web/modules/contrib/ctools/src/ContextMapper.php new file mode 100644 index 000000000..284dcf070 --- /dev/null +++ b/web/modules/contrib/ctools/src/ContextMapper.php @@ -0,0 +1,50 @@ +entityRepository = $entity_repository; + } + + /** + * {@inheritdoc} + */ + public function getContextValues(array $context_configurations) { + $contexts = []; + foreach ($context_configurations as $name => $context_configuration) { + $context_definition = new ContextDefinition($context_configuration['type'], $context_configuration['label'], TRUE, FALSE, $context_configuration['description']); + if (strpos($context_configuration['type'], 'entity:') === 0) { + $context = new EntityLazyLoadContext($context_definition, $this->entityRepository, $context_configuration['value']); + } + else { + $context = new Context($context_definition, $context_configuration['value']); + } + $contexts[$name] = $context; + } + return $contexts; + } + +} diff --git a/web/modules/contrib/ctools/src/ContextMapperInterface.php b/web/modules/contrib/ctools/src/ContextMapperInterface.php new file mode 100644 index 000000000..b583817e7 --- /dev/null +++ b/web/modules/contrib/ctools/src/ContextMapperInterface.php @@ -0,0 +1,21 @@ +entityManager = $manager; + } + + /** + * {@inheritdoc} + */ + protected function getFormArgument(RouteMatchInterface $route_match) { + $form_arg = $route_match->getRouteObject()->getDefault('_entity_wizard'); + list($entity_type_id, $operation) = explode('.', $form_arg); + $definition = $this->entityManager->getDefinition($entity_type_id); + $handlers = $definition->getHandlerClasses(); + if (empty($handlers['wizard'][$operation])) { + throw new \Exception(sprintf('Unsupported wizard operation %s', $operation)); + } + return $handlers['wizard'][$operation]; + } + +} diff --git a/web/modules/contrib/ctools/src/Controller/WizardFormController.php b/web/modules/contrib/ctools/src/Controller/WizardFormController.php new file mode 100644 index 000000000..e4492a4c9 --- /dev/null +++ b/web/modules/contrib/ctools/src/Controller/WizardFormController.php @@ -0,0 +1,82 @@ +wizardFactory = $wizard_factory; + } + + /** + * {@inheritdoc} + */ + protected function getFormArgument(RouteMatchInterface $route_match) { + return $route_match->getRouteObject()->getDefault('_wizard'); + } + + /** + * Wizards are not instantiated as simply as forms, so this method is unused. + */ + protected function getFormObject(RouteMatchInterface $route_match, $form_arg) { + if (!is_subclass_of($form_arg, '\Drupal\ctools\Wizard\FormWizardInterface')) { + throw new \Exception("The _wizard default must reference a class instance of \\Drupal\\ctools\\Wizard\\FormWizardInterface."); + } + $parameters = $route_match->getParameters()->all(); + $parameters += $form_arg::getParameters(); + $parameters['route_match'] = $route_match; + return $this->wizardFactory->createWizard($form_arg, $parameters); + } + + /** + * {@inheritdoc} + */ + public function getContentResult(Request $request, RouteMatchInterface $route_match) { + $wizard = $this->getFormObject($route_match, $this->getFormArgument($route_match)); + $ajax = $request->attributes->get('js') == 'ajax' ? TRUE : FALSE; + + return $this->wizardFactory->getWizardForm($wizard, $request->attributes->all(), $ajax); + } + +} diff --git a/web/modules/contrib/ctools/src/Event/WizardEvent.php b/web/modules/contrib/ctools/src/Event/WizardEvent.php new file mode 100644 index 000000000..7123297e3 --- /dev/null +++ b/web/modules/contrib/ctools/src/Event/WizardEvent.php @@ -0,0 +1,41 @@ +wizard = $wizard; + $this->values = $values; + } + + public function getWizard() { + return $this->wizard; + } + + public function getValues() { + return $this->values; + } + + public function setValues($values) { + $this->values = $values; + return $this; + } + +} diff --git a/web/modules/contrib/ctools/src/Form/AjaxFormTrait.php b/web/modules/contrib/ctools/src/Form/AjaxFormTrait.php new file mode 100644 index 000000000..b30250bbb --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/AjaxFormTrait.php @@ -0,0 +1,43 @@ + ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 'auto', + ]), + ]; + } + + /** + * Gets attributes for use with an add button AJAX modal. + * + * @return array + */ + public static function getAjaxButtonAttributes() { + return NestedArray::mergeDeep(AjaxFormTrait::getAjaxAttributes(), [ + 'class' => [ + 'button', + 'button--small', + 'button-action', + ], + ]); + } + +} diff --git a/web/modules/contrib/ctools/src/Form/ConditionConfigure.php b/web/modules/contrib/ctools/src/Form/ConditionConfigure.php new file mode 100644 index 000000000..fd2608e13 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ConditionConfigure.php @@ -0,0 +1,182 @@ +get('user.shared_tempstore'), $container->get('plugin.manager.condition')); + } + + function __construct(SharedTempStoreFactory $tempstore, PluginManagerInterface $manager) { + $this->tempstore = $tempstore; + $this->manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_condition_configure'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $condition = NULL, $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + if (is_numeric($condition) || Uuid::isValid($condition)) { + $id = $condition; + $condition = $this->getConditions($cached_values)[$id]; + $instance = $this->manager->createInstance($condition['id'], $condition); + } + else { + $instance = $this->manager->createInstance($condition, []); + } + $form_state->setTemporaryValue('gathered_contexts', $this->getContexts($cached_values)); + /** @var $instance \Drupal\Core\Condition\ConditionInterface */ + $form = $instance->buildConfigurationForm($form, $form_state); + if (isset($id)) { + // Conditionally set this form element so that we can update or add. + $form['id'] = [ + '#type' => 'value', + '#value' => $id + ]; + } + $form['instance'] = [ + '#type' => 'value', + '#value' => $instance + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#ajax' => [ + 'callback' => [$this, 'ajaxSave'], + ] + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + /** @var $instance \Drupal\Core\Condition\ConditionInterface */ + $instance = $form_state->getValue('instance'); + $instance->submitConfigurationForm($form, $form_state); + $conditions = $this->getConditions($cached_values); + if ($instance instanceof ContextAwarePluginInterface) { + /** @var $instance \Drupal\Core\Plugin\ContextAwarePluginInterface */ + $context_mapping = $form_state->hasValue('context_mapping')? $form_state->getValue('context_mapping') : []; + $instance->setContextMapping($context_mapping); + } + if ($instance instanceof ConstraintConditionInterface) { + /** @var $instance \Drupal\ctools\ConstraintConditionInterface */ + $instance->applyConstraints($this->getContexts($cached_values)); + } + if ($form_state->hasValue('id')) { + $conditions[$form_state->getValue('id')] = $instance->getConfiguration(); + } + else { + $conditions[] = $instance->getConfiguration(); + } + $cached_values = $this->setConditions($cached_values, $conditions); + $this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $form_state->setRedirect($route_name, $route_parameters); + } + + public function ajaxSave(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $response->addCommand(new RedirectCommand($this->url($route_name, $route_parameters))); + $response->addCommand(new CloseModalDialogCommand()); + return $response; + } + + /** + * Document the route name and parameters for redirect after submission. + * + * @param $cached_values + * + * @return array + * In the format of + * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name']]; + */ + abstract protected function getParentRouteInfo($cached_values); + + /** + * Custom logic for retrieving the conditions array from cached_values. + * + * @param $cached_values + * + * @return array + */ + abstract protected function getConditions($cached_values); + + /** + * Custom logic for setting the conditions array in cached_values. + * + * @param $cached_values + * + * @param $conditions + * The conditions to set within the cached values. + * + * @return mixed + * Return the $cached_values + */ + abstract protected function setConditions($cached_values, $conditions); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract protected function getContexts($cached_values); + +} diff --git a/web/modules/contrib/ctools/src/Form/ConditionDelete.php b/web/modules/contrib/ctools/src/Form/ConditionDelete.php new file mode 100644 index 000000000..d2303bc86 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ConditionDelete.php @@ -0,0 +1,210 @@ +get('user.shared_tempstore'), $container->get('plugin.manager.condition')); + } + + function __construct(SharedTempStoreFactory $tempstore, PluginManagerInterface $manager) { + $this->tempstore = $tempstore; + $this->manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_condition_delete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $this->id = $id; + + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $form ['#title'] = $this->getQuestion($id, $cached_values); + + $form ['#attributes']['class'][] = 'confirmation'; + $form ['description'] = array('#markup' => $this->getDescription()); + $form [$this->getFormName()] = array('#type' => 'hidden', '#value' => 1); + + // By default, render the form using theme_confirm_form(). + if (!isset($form ['#theme'])) { + $form ['#theme'] = 'confirm_form'; + } + $form['actions'] = array('#type' => 'actions'); + $form['actions'] += $this->actions($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $conditions = $this->getConditions($cached_values); + /** @var $instance \Drupal\ctools\ConstraintConditionInterface */ + $instance = $this->manager->createInstance($conditions[$this->id]['id'], $conditions[$this->id]); + if ($instance instanceof ConstraintConditionInterface) { + $instance->removeConstraints($this->getContexts($cached_values)); + } + unset($conditions[$this->id]); + $cached_values = $this->setConditions($cached_values, $conditions); + $this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $form_state->setRedirect($route_name, $route_parameters); + } + + public function getQuestion($id = NULL, $cached_values = NULL) { + $condition = $this->getConditions($cached_values)[$id]; + return $this->t('Are you sure you want to delete the @label condition?', array( + '@label' => $condition['id'], + )); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This action cannot be undone.'); + } + + /** + * {@inheritdoc} + */ + public function getFormName() { + return 'confirm'; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + return array( + 'submit' => array( + '#type' => 'submit', + '#value' => $this->getConfirmText(), + '#validate' => array( + array($this, 'validateForm'), + ), + '#submit' => array( + array($this, 'submitForm'), + ), + ), + 'cancel' => ConfirmFormHelper::buildCancelLink($this, $this->getRequest()), + ); + } + + /** + * Returns the route to go to if the user cancels the action. + * + * @return \Drupal\Core\Url + * A URL object. + */ + public function getCancelUrl() { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + return new Url($route_name, $route_parameters); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function getCancelText() { + return $this->t('Cancel'); + } + + /** + * Document the route name and parameters for redirect after submission. + * + * @param $cached_values + * + * @return array + * In the format of + * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]]; + */ + abstract protected function getParentRouteInfo($cached_values); + + /** + * Custom logic for retrieving the conditions array from cached_values. + * + * @param $cached_values + * + * @return array + */ + abstract protected function getConditions($cached_values); + + /** + * Custom logic for setting the conditions array in cached_values. + * + * @param $cached_values + * + * @param $conditions + * The conditions to set within the cached values. + * + * @return mixed + * Return the $cached_values + */ + abstract protected function setConditions($cached_values, $conditions); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract protected function getContexts($cached_values); + +} diff --git a/web/modules/contrib/ctools/src/Form/ContextConfigure.php b/web/modules/contrib/ctools/src/Form/ContextConfigure.php new file mode 100644 index 000000000..142f18abb --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ContextConfigure.php @@ -0,0 +1,254 @@ +get('user.shared_tempstore')); + } + + function __construct(SharedTempStoreFactory $tempstore) { + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_context_configure'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $context_id = NULL, $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $contexts = $this->getContexts($cached_values); + $edit = FALSE; + if (!empty($contexts[$context_id])) { + $context = $contexts[$context_id]; + $machine_name = $context_id; + $edit = TRUE; + } + else { + $context_definition = new ContextDefinition($context_id); + $context = new Context($context_definition); + $machine_name = ''; + } + $label = $context->getContextDefinition()->getLabel(); + $description = $context->getContextDefinition()->getDescription(); + $data_type = $context->getContextDefinition()->getDataType(); + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $form['context_id'] = [ + '#type' => 'value', + '#value' => $context_id + ]; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#default_value' => $label, + '#required' => TRUE, + ]; + $form['machine_name'] = [ + '#type' => 'machine_name', + '#title' => $this->t('Machine Name'), + '#default_value' => $machine_name, + '#required' => TRUE, + '#maxlength' => 128, + '#machine_name' => [ + 'source' => ['label'], + 'exists' => [$this, 'contextExists'], + ], + '#disabled' => $this->disableMachineName($cached_values, $machine_name), + ]; + $form['description'] = [ + '#type' => 'textarea', + '#title' => $this->t('Description'), + '#default_value' => $description, + ]; + if (strpos($data_type, 'entity:') === 0) { + list(, $entity_type) = explode(':', $data_type); + /** @var EntityAdapter $entity */ + $entity = $edit ? $context->getContextValue() : NULL; + $form['context_value'] = [ + '#type' => 'entity_autocomplete', + '#required' => TRUE, + '#target_type' => $entity_type, + '#default_value' => $entity, + '#title' => $this->t('Select entity'), + ]; + } + else { + $value = $context->getContextData()->getValue(); + $form['context_value'] = [ + '#title' => $this->t('Set a context value'), + '#type' => 'textfield', + '#required' => TRUE, + '#default_value' => $value, + ]; + } + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#ajax' => [ + 'callback' => [$this, 'ajaxSave'], + ] + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // If these are not equal, then we're adding a new context and should not override an existing context. + if ($form_state->getValue('machine_name') != $form_state->getValue('context_id')) { + $machine_name = $form_state->getValue('machine_name'); + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + if (!empty($this->getContexts($cached_values)[$machine_name])) { + $form_state->setError($form['machine_name'], $this->t('That machine name is in use by another context definition.')); + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $contexts = $this->getContexts($cached_values); + if ($form_state->getValue('machine_name') != $form_state->getValue('context_id')) { + $data_type = $form_state->getValue('context_id'); + $context_definition = new ContextDefinition($data_type, $form_state->getValue('label'), TRUE, FALSE, $form_state->getValue('description')); + } + else { + $context = $contexts[$form_state->getValue('machine_name')]; + $context_definition = $context->getContextDefinition(); + $context_definition->setLabel($form_state->getValue('label')); + $context_definition->setDescription($form_state->getValue('description')); + } + // We're dealing with an entity and should make sure it's loaded. + if (strpos($context_definition->getDataType(), 'entity:') === 0) { + list(, $entity_type) = explode(':', $context_definition->getDataType()); + if (is_numeric($form_state->getValue('context_value'))) { + $value = \Drupal::entityTypeManager()->getStorage($entity_type)->load($form_state->getValue('context_value')); + } + } + // No loading required for non-entity values. + else { + $value = $form_state->getValue('context_value'); + } + $context = new Context($context_definition, $value); + + $cached_values = $this->addContext($cached_values, $form_state->getValue('machine_name'), $context); + $this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $form_state->setRedirect($route_name, $route_parameters); + } + + public function ajaxSave(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $url = new Url($route_name, $route_parameters); + $response->addCommand(new RedirectCommand($url->toString())); + $response->addCommand(new CloseModalDialogCommand()); + return $response; + } + + /** + * Document the route name and parameters for redirect after submission. + * + * @param $cached_values + * + * @return array + * In the format of + * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]]; + */ + abstract protected function getParentRouteInfo($cached_values); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract protected function getContexts($cached_values); + + /** + * Custom logic for adding a context to the cached_values contexts array. + * + * @param array $cached_values + * The cached_values currently in use. + * @param string $context_id + * The context identifier. + * @param \Drupal\Core\Plugin\Context\ContextInterface $context + * The context to add or update within the cached values. + * + * @return mixed + * Return the $cached_values + */ + abstract protected function addContext($cached_values, $context_id, ContextInterface $context); + + /** + * Custom "exists" logic for the context to be created. + * + * @param string $value + * The name of the context. + * @param $element + * The machine_name element + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return bool + * Return true if a context of this name exists. + */ + abstract public function contextExists($value, $element, $form_state); + + /** + * Determines if the machine_name should be disabled. + * + * @param $cached_values + * + * @return bool + */ + abstract protected function disableMachineName($cached_values, $machine_name); + +} diff --git a/web/modules/contrib/ctools/src/Form/ContextDelete.php b/web/modules/contrib/ctools/src/Form/ContextDelete.php new file mode 100644 index 000000000..06e0794a8 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ContextDelete.php @@ -0,0 +1,85 @@ +get('user.shared_tempstore')); + } + + public function __construct(SharedTempStoreFactory $tempstore) { + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_context_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $tempstore_id = NULL, $machine_name = NULL, $context_id = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $this->context_id = $context_id; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $form_state->setRedirectUrl($this->getCancelUrl()); + } + + protected function getTempstore() { + return $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + } + + protected function setTempstore($cached_values) { + $this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values); + } + +} diff --git a/web/modules/contrib/ctools/src/Form/ManageConditions.php b/web/modules/contrib/ctools/src/Form/ManageConditions.php new file mode 100644 index 000000000..ee506a89d --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ManageConditions.php @@ -0,0 +1,224 @@ +get('plugin.manager.condition')); + } + + function __construct(PluginManagerInterface $manager) { + $this->manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_manage_conditions_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $this->machine_name = $cached_values['id']; + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $options = []; + $contexts = $this->getContexts($cached_values); + foreach ($this->manager->getDefinitionsForContexts($contexts) as $plugin_id => $definition) { + $options[$plugin_id] = (string) $definition['label']; + } + $form['items'] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + '#theme' => 'table', + '#header' => array($this->t('Plugin Id'), $this->t('Summary'), $this->t('Operations')), + '#rows' => $this->renderRows($cached_values), + '#empty' => $this->t('No required conditions have been configured.') + ); + $form['conditions'] = [ + '#type' => 'select', + '#options' => $options, + ]; + $form['add'] = [ + '#type' => 'submit', + '#name' => 'add', + '#value' => $this->t('Add Condition'), + '#ajax' => [ + 'callback' => [$this, 'add'], + 'event' => 'click', + ], + '#submit' => [ + 'callback' => [$this, 'submitForm'], + ] + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions')); + $form_state->setRedirect($this->getAddRoute($cached_values), $route_parameters); + } + + public function add(array &$form, FormStateInterface $form_state) { + $condition = $form_state->getValue('conditions'); + $content = \Drupal::formBuilder()->getForm($this->getConditionClass(), $condition, $this->getTempstoreId(), $this->machine_name); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions')); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Configure Required Context'), $content, array('width' => '700'))); + return $response; + } + + /** + * @param $cached_values + * + * @return array + */ + public function renderRows($cached_values) { + $configured_conditions = array(); + foreach ($this->getConditions($cached_values) as $row => $condition) { + /** @var $instance \Drupal\Core\Condition\ConditionInterface */ + $instance = $this->manager->createInstance($condition['id'], $condition); + list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row); + $build = array( + '#type' => 'operations', + '#links' => $this->getOperations($route_name, $route_parameters), + ); + $configured_conditions[] = array( + $instance->getPluginId(), + $instance->summary(), + 'operations' => [ + 'data' => $build, + ], + ); + } + return $configured_conditions; + } + + protected function getOperations($route_name_base, array $route_parameters = array()) { + $operations['edit'] = array( + 'title' => $this->t('Edit'), + 'url' => new Url($route_name_base . '.edit', $route_parameters), + 'weight' => 10, + 'attributes' => array( + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ), + ); + $route_parameters['id'] = $route_parameters['condition']; + $operations['delete'] = array( + 'title' => $this->t('Delete'), + 'url' => new Url($route_name_base . '.delete', $route_parameters), + 'weight' => 100, + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ), + ); + return $operations; + } + + /** + * Return a subclass of '\Drupal\ctools\Form\ConditionConfigure'. + * + * The ConditionConfigure class is designed to be subclassed with custom + * route information to control the modal/redirect needs of your use case. + * + * @return string + */ + abstract protected function getConditionClass(); + + /** + * The route to which condition 'add' actions should submit. + * + * @param mixed $cached_values + * + * @return string + */ + abstract protected function getAddRoute($cached_values); + + /** + * Provide the tempstore id for your specified use case. + * + * @return string + */ + abstract protected function getTempstoreId(); + + /** + * Document the route name and parameters for edit/delete context operations. + * + * The route name returned from this method is used as a "base" to which + * ".edit" and ".delete" are appeneded in the getOperations() method. + * Subclassing '\Drupal\ctools\Form\ConditionConfigure' and + * '\Drupal\ctools\Form\ConditionDelete' should set you up for using this + * approach quite seamlessly. + * + * @param mixed $cached_values + * + * @param string $machine_name + * + * @param string $row + * + * @return array + * In the format of + * return ['route.base.name', ['machine_name' => $machine_name, 'context' => $row]]; + */ + abstract protected function getOperationsRouteInfo($cached_values, $machine_name, $row); + + /** + * Custom logic for retrieving the conditions array from cached_values. + * + * @param $cached_values + * + * @return array + */ + abstract protected function getConditions($cached_values); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract protected function getContexts($cached_values); + +} diff --git a/web/modules/contrib/ctools/src/Form/ManageContext.php b/web/modules/contrib/ctools/src/Form/ManageContext.php new file mode 100644 index 000000000..4ac3b8438 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ManageContext.php @@ -0,0 +1,327 @@ +get('typed_data_manager'), $container->get('form_builder')); + } + + /** + * ManageContext constructor. + * + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder. + */ + public function __construct(TypedDataManagerInterface $typed_data_manager, FormBuilderInterface $form_builder) { + $this->typedDataManager = $typed_data_manager; + $this->formBuilder = $form_builder; + } + + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_manage_context_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $this->machine_name = $cached_values['id']; + $form['items'] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + '#theme' => 'table', + '#header' => array($this->t('Context ID'), $this->t('Label'), $this->t('Data Type'), $this->t('Options')), + '#rows' => $this->renderRows($cached_values), + '#empty' => $this->t('No contexts or relationships have been added.') + ); + foreach ($this->typedDataManager->getDefinitions() as $type => $definition) { + $types[$type] = $definition['label']; + } + if (isset($types['entity'])) { + unset($types['entity']); + } + asort($types); + $form['context'] = [ + '#type' => 'select', + '#options' => $types, + ]; + $form['add'] = [ + '#type' => 'submit', + '#name' => 'add', + '#value' => $this->t('Add new context'), + '#ajax' => [ + 'callback' => [$this, 'addContext'], + 'event' => 'click', + ], + '#submit' => [ + 'callback' => [$this, 'submitForm'], + ] + ]; + + $form['relationships'] = [ + '#type' => 'select', + '#title' => $this->t('Add a relationship'), + '#options' => $this->getAvailableRelationships($cached_values), + '#access' => $this->relationships, + ]; + $form['add_relationship'] = [ + '#type' => 'submit', + '#name' => 'add_relationship', + '#value' => $this->t('Add Relationship'), + '#ajax' => [ + 'callback' => [$this, 'addRelationship'], + 'event' => 'click', + ], + '#submit' => [ + 'callback' => [$this, 'submitForm'], + ], + '#access' => $this->relationships, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getTriggeringElement()['#name'] == 'add') { + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('context')); + $form_state->setRedirect($this->getContextAddRoute($cached_values), $route_parameters); + } + if ($form_state->getTriggeringElement()['#name'] == 'add_relationship') { + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships')); + $form_state->setRedirect($this->getRelationshipAddRoute($cached_values), $route_parameters); + } + } + + public function addContext(array &$form, FormStateInterface $form_state) { + $context = $form_state->getValue('context'); + $content = $this->formBuilder->getForm($this->getContextClass(), $context, $this->getTempstoreId(), $this->machine_name); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getContextAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Add new context'), $content, array('width' => '700'))); + return $response; + } + + public function addRelationship(array &$form, FormStateInterface $form_state) { + $relationship = $form_state->getValue('relationships'); + $content = $this->formBuilder->getForm($this->getRelationshipClass(), $relationship, $this->getTempstoreId(), $this->machine_name); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getRelationshipAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Configure Relationship'), $content, array('width' => '700'))); + return $response; + } + + protected function getAvailableRelationships($cached_values) { + /** @var \Drupal\ctools\TypedDataResolver $resolver */ + $resolver = \Drupal::service('ctools.typed_data.resolver'); + return $resolver->getTokensForContexts($this->getContexts($cached_values)); + } + + /** + * @param $cached_values + * + * @return array + */ + protected function renderRows($cached_values) { + $contexts = array(); + foreach ($this->getContexts($cached_values) as $row => $context) { + list($route_name, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $row); + $build = array( + '#type' => 'operations', + '#links' => $this->getOperations($cached_values, $row, $route_name, $route_parameters), + ); + $contexts[$row] = array( + $row, + $context->getContextDefinition()->getLabel(), + $context->getContextDefinition()->getDataType(), + 'operations' => [ + 'data' => $build, + ], + ); + } + return $contexts; + } + + /** + * @param array $cached_values + * @param string $row + * @param string $route_name_base + * @param array $route_parameters + * + * @return mixed + */ + protected function getOperations($cached_values, $row, $route_name_base, array $route_parameters = array()) { + $operations = []; + if ($this->isEditableContext($cached_values, $row)) { + $operations['edit'] = array( + 'title' => $this->t('Edit'), + 'url' => new Url($route_name_base . '.edit', $route_parameters), + 'weight' => 10, + 'attributes' => array( + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ), + ); + $operations['delete'] = array( + 'title' => $this->t('Delete'), + 'url' => new Url($route_name_base . '.delete', $route_parameters), + 'weight' => 100, + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ), + ); + } + return $operations; + } + + /** + * Return a subclass of '\Drupal\ctools\Form\ContextConfigure'. + * + * The ContextConfigure class is designed to be subclassed with custom + * route information to control the modal/redirect needs of your use case. + * + * @return string + */ + abstract protected function getContextClass($cached_values); + + /** + * Return a subclass of '\Drupal\ctools\Form\RelationshipConfigure'. + * + * The RelationshipConfigure class is designed to be subclassed with custom + * route information to control the modal/redirect needs of your use case. + * + * @return string + */ + abstract protected function getRelationshipClass($cached_values); + + /** + * The route to which context 'add' actions should submit. + * + * @return string + */ + abstract protected function getContextAddRoute($cached_values); + + /** + * The route to which relationship 'add' actions should submit. + * + * @return string + */ + abstract protected function getRelationshipAddRoute($cached_values); + + /** + * Provide the tempstore id for your specified use case. + * + * @return string + */ + abstract protected function getTempstoreId(); + + /** + * Returns the contexts already available in the wizard. + * + * @param mixed $cached_values + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract protected function getContexts($cached_values); + + /** + * @param mixed $cached_values + * @param string $machine_name + * @param string $row + * + * @return array + */ + abstract protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row); + + /** + * @param mixed $cached_values + * @param string $machine_name + * @param string $row + * + * @return array + */ + abstract protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row); + + /** + * @param mixed $cached_values + * @param string $row + * + * @return bool + */ + abstract protected function isEditableContext($cached_values, $row); + +} diff --git a/web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php b/web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php new file mode 100644 index 000000000..7dfff5982 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ManageResolverRelationships.php @@ -0,0 +1,205 @@ +getTemporaryValue('wizard'); + $this->machine_name = $cached_values['id']; + $form['items'] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + '#theme' => 'table', + '#header' => array($this->t('Context ID'), $this->t('Label'), $this->t('Data Type'), $this->t('Options')), + '#rows' => $this->renderRows($cached_values), + '#empty' => $this->t('No relationships have been added.') + ); + + $form['relationships'] = [ + '#type' => 'select', + '#title' => $this->t('Add a relationship'), + '#options' => $this->getAvailableRelationships($cached_values), + ]; + $form['add_relationship'] = [ + '#type' => 'submit', + '#name' => 'add', + '#value' => $this->t('Add Relationship'), + '#ajax' => [ + 'callback' => [$this, 'addRelationship'], + 'event' => 'click', + ], + '#submit' => [ + 'callback' => [$this, 'submitForm'], + ] + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getTriggeringElement()['#name'] == 'add') { + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('relationships')); + $form_state->setRedirect($this->getAddRoute($cached_values), $route_parameters); + } + } + + public function addRelationship(array &$form, FormStateInterface $form_state) { + $relationship = $form_state->getValue('relationships'); + $content = \Drupal::formBuilder()->getForm($this->getContextClass(), $relationship, $this->getTempstoreId(), $this->machine_name); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $cached_values = $form_state->getTemporaryValue('wizard'); + list(, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $relationship); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Configure Relationship'), $content, array('width' => '700'))); + return $response; + } + + protected function getAvailableRelationships($cached_values) { + /** @var \Drupal\ctools\TypedDataResolver $resolver */ + $resolver = \Drupal::service('ctools.typed_data.resolver'); + return $resolver->getTokensForContexts($this->getContexts($cached_values)); + } + + /** + * @param $cached_values + * + * @return array + */ + protected function renderRows($cached_values) { + $contexts = array(); + foreach ($this->getContexts($cached_values) as $row => $context) { + list($route_name, $route_parameters) = $this->getRelationshipOperationsRouteInfo($cached_values, $this->machine_name, $row); + $build = array( + '#type' => 'operations', + '#links' => $this->getOperations($cached_values, $row, $route_name, $route_parameters), + ); + $contexts[$row] = array( + $row, + $context->getContextDefinition()->getLabel(), + $context->getContextDefinition()->getDataType(), + 'operations' => [ + 'data' => $build, + ], + ); + } + return $contexts; + } + + /** + * @param array $cached_values + * @param string $row + * @param string $route_name_base + * @param array $route_parameters + * + * @return mixed + */ + protected function getOperations($cached_values, $row, $route_name_base, array $route_parameters = array()) { + // Base contexts will not be a : separated and generated relationships should have 3 parts. + if (count(explode(':', $row)) < 2) { + return []; + } + $operations['edit'] = array( + 'title' => $this->t('Edit'), + 'url' => new Url($route_name_base . '.edit', $route_parameters), + 'weight' => 10, + 'attributes' => array( + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ), + ); + $route_parameters['id'] = $route_parameters['context']; + $operations['delete'] = array( + 'title' => $this->t('Delete'), + 'url' => new Url($route_name_base . '.delete', $route_parameters), + 'weight' => 100, + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ), + ); + return $operations; + } + + /** + * Return a subclass of '\Drupal\ctools\Form\ResolverRelationshipConfigure'. + * + * The ConditionConfigure class is designed to be subclassed with custom + * route information to control the modal/redirect needs of your use case. + * + * @return string + */ + abstract protected function getContextClass($cached_values); + + /** + * The route to which relationship 'add' actions should submit. + * + * @return string + */ + abstract protected function getAddRoute($cached_values); + + /** + * Provide the tempstore id for your specified use case. + * + * @return string + */ + abstract protected function getTempstoreId(); + + /** + * @param $cached_values + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract protected function getContexts($cached_values); + + /** + * @param string $cached_values + * @param string $machine_name + * @param string $row + * + * @return array + */ + abstract protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row); + +} diff --git a/web/modules/contrib/ctools/src/Form/RelationshipConfigure.php b/web/modules/contrib/ctools/src/Form/RelationshipConfigure.php new file mode 100644 index 000000000..4f13f46e8 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/RelationshipConfigure.php @@ -0,0 +1,152 @@ +get('user.shared_tempstore'), $container->get('ctools.typed_data.resolver')); + } + + public function __construct(SharedTempStoreFactory $tempstore, TypedDataResolver $resolver) { + $this->tempstore = $tempstore; + $this->resolver = $resolver; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_relationship_configure'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $context_id = NULL, $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + + /** @var \Drupal\Core\Plugin\Context\ContextInterface[] $contexts */ + $contexts = $this->getContexts($cached_values); + $context_object = $this->resolver->convertTokenToContext($context_id, $contexts); + $form['id'] = [ + '#type' => 'value', + '#value' => $context_id + ]; + $form['context_object'] = [ + '#type' => 'value', + '#value' => $context_object, + ]; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Context label'), + '#default_value' => !empty($contexts[$context_id]) ? $contexts[$context_id]->getContextDefinition()->getLabel() : $this->resolver->getLabelByToken($context_id, $contexts), + '#required' => TRUE, + ]; + $form['context_data'] = [ + '#type' => 'item', + '#title' => $this->t('Data type'), + '#markup' => $context_object->getContextDefinition()->getDataType(), + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#ajax' => [ + 'callback' => [$this, 'ajaxSave'], + ] + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + list($route_name, $route_options) = $this->getParentRouteInfo($cached_values); + $form_state->setRedirect($route_name, $route_options); + } + + /** + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return \Drupal\Core\Ajax\AjaxResponse + */ + public function ajaxSave(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $response = new AjaxResponse(); + $response->addCommand(new RedirectCommand($this->url($route_name, $route_parameters))); + $response->addCommand(new CloseModalDialogCommand()); + return $response; + } + + /** + * Document the route name and parameters for redirect after submission. + * + * @param array $cached_values + * + * @return array In the format of + * In the format of + * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name']]; + */ + abstract protected function getParentRouteInfo($cached_values); + + /** + * Custom logic for setting the conditions array in cached_values. + * + * @param $cached_values + * + * @param $contexts + * The conditions to set within the cached values. + * + * @return mixed + * Return the $cached_values + */ + abstract protected function setContexts($cached_values, $contexts); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract protected function getContexts($cached_values); + +} diff --git a/web/modules/contrib/ctools/src/Form/RequiredContext.php b/web/modules/contrib/ctools/src/Form/RequiredContext.php new file mode 100644 index 000000000..76b5ec2da --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/RequiredContext.php @@ -0,0 +1,212 @@ +get('typed_data_manager')); + } + + public function __construct(PluginManagerInterface $typed_data_manager) { + $this->typedDataManager = $typed_data_manager; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_required_context_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $this->machine_name = $cached_values['id']; + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $options = []; + foreach ($this->typedDataManager->getDefinitions() as $plugin_id => $definition) { + $options[$plugin_id] = (string) $definition['label']; + } + $form['items'] = array( + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + '#theme' => 'table', + '#header' => array($this->t('Information'), $this->t('Description'), $this->t('Operations')), + '#rows' => $this->renderContexts($cached_values), + '#empty' => $this->t('No required contexts have been configured.') + ); + $form['contexts'] = [ + '#type' => 'select', + '#options' => $options, + ]; + $form['add'] = [ + '#type' => 'submit', + '#name' => 'add', + '#value' => $this->t('Add required context'), + '#ajax' => [ + 'callback' => [$this, 'add'], + 'event' => 'click', + ], + '#submit' => [ + 'callback' => [$this, 'submitform'], + ] + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('contexts')); + $form_state->setRedirect($route_name . '.edit', $route_parameters); + } + + /** + * Custom ajax form submission handler. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return \Drupal\Core\Ajax\AjaxResponse + */ + public function add(array &$form, FormStateInterface $form_state) { + $context = $form_state->getValue('contexts'); + $content = \Drupal::formBuilder()->getForm($this->getContextClass(), $context, $this->getTempstoreId(), $this->machine_name); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Configure Required Context'), $content, array('width' => '700'))); + return $response; + } + + /** + * @param $cached_values + * + * @return array + */ + public function renderContexts($cached_values) { + $configured_contexts = array(); + foreach ($this->getContexts($cached_values) as $row => $context) { + list($plugin_id, $label, $machine_name, $description) = array_values($context); + list($route_name, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $row); + $build = array( + '#type' => 'operations', + '#links' => $this->getOperations($route_name, $route_parameters), + ); + $configured_contexts[] = array( + $this->t('Label: @label
Type: @type', ['@label' => $label, '@type' => $plugin_id]), + $this->t('@description', ['@description' => $description]), + 'operations' => [ + 'data' => $build, + ], + ); + } + return $configured_contexts; + } + + protected function getOperations($route_name_base, array $route_parameters = array()) { + $operations['edit'] = array( + 'title' => $this->t('Edit'), + 'url' => new Url($route_name_base . '.edit', $route_parameters), + 'weight' => 10, + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + 'data-dialog-options' => json_encode(array( + 'width' => 700, + )), + ), + 'ajax' => [ + '' + ], + ); + $route_parameters['id'] = $route_parameters['context']; + $operations['delete'] = array( + 'title' => $this->t('Delete'), + 'url' => new Url($route_name_base . '.delete', $route_parameters), + 'weight' => 100, + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + 'data-dialog-options' => json_encode(array( + 'width' => 700, + )), + ), + ); + return $operations; + } + + /** + * Return a subclass of '\Drupal\ctools\Form\ContextConfigure'. + * + * The ContextConfigure class is designed to be subclassed with custom route + * information to control the modal/redirect needs of your use case. + * + * @return string + */ + abstract protected function getContextClass(); + + /** + * Provide the tempstore id for your specified use case. + * + * @return string + */ + abstract protected function getTempstoreId(); + + /** + * Document the route name and parameters for edit/delete context operations. + * + * The route name returned from this method is used as a "base" to which + * ".edit" and ".delete" are appeneded in the getOperations() method. + * Subclassing '\Drupal\ctools\Form\ContextConfigure' and + * '\Drupal\ctools\Form\RequiredContextDelete' should set you up for using + * this approach quite seamlessly. + * + * @param mixed $cached_values + * + * @param string $machine_name + * + * @param string $row + * + * @return array + * In the format of + * return ['route.base.name', ['machine_name' => $machine_name, 'context' => $row]]; + */ + abstract protected function getOperationsRouteInfo($cached_values, $machine_name, $row); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return array + */ + abstract protected function getContexts($cached_values); + +} diff --git a/web/modules/contrib/ctools/src/Form/RequiredContextDelete.php b/web/modules/contrib/ctools/src/Form/RequiredContextDelete.php new file mode 100644 index 000000000..e04429a98 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/RequiredContextDelete.php @@ -0,0 +1,194 @@ +get('user.shared_tempstore')); + } + + /** + * @param \Drupal\user\SharedTempStoreFactory $tempstore + */ + function __construct(SharedTempStoreFactory $tempstore) { + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_required_context_delete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $this->id = $id; + + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $form ['#title'] = $this->getQuestion($id, $cached_values); + + $form ['#attributes']['class'][] = 'confirmation'; + $form ['description'] = array('#markup' => $this->getDescription()); + $form [$this->getFormName()] = array('#type' => 'hidden', '#value' => 1); + + // By default, render the form using theme_confirm_form(). + if (!isset($form ['#theme'])) { + $form ['#theme'] = 'confirm_form'; + } + $form['actions'] = array('#type' => 'actions'); + $form['actions'] += $this->actions($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $contexts = $this->getContexts($cached_values); + unset($contexts[$this->id]); + $cached_values = $this->setContexts($cached_values, $contexts); + $this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $form_state->setRedirect($route_name, $route_parameters); + } + + /** + * {@inheritdoc} + */ + public function getQuestion($id = NULL, $cached_values = NULL) { + $context = $this->getContexts($cached_values)[$id]; + return $this->t('Are you sure you want to delete the @label context?', array( + '@label' => $context['label'], + )); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This action cannot be undone.'); + } + + /** + * {@inheritdoc} + */ + public function getFormName() { + return 'confirm'; + } + + /** + * Provides the action buttons for submitting this form. + */ + protected function actions(array $form, FormStateInterface $form_state) { + return array( + 'submit' => array( + '#type' => 'submit', + '#value' => $this->getConfirmText(), + '#validate' => array( + array($this, 'validate'), + ), + '#submit' => array( + array($this, 'submitForm'), + ), + ), + 'cancel' => ConfirmFormHelper::buildCancelLink($this, $this->getRequest()), + ); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + return new Url($route_name, $route_parameters); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function getCancelText() { + return $this->t('Cancel'); + } + + /** + * Document the route name and parameters for redirect after submission. + * + * @param $cached_values + * + * @return array + * In the format of + * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]]; + */ + abstract protected function getParentRouteInfo($cached_values); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return array + */ + abstract protected function getContexts($cached_values); + + /** + * Custom logic for setting the contexts array in cached_values. + * + * @param $cached_values + * + * @param $contexts + * The contexts to set within the cached values. + * + * @return mixed + * Return the $cached_values + */ + abstract protected function setContexts($cached_values, $contexts); + +} diff --git a/web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php b/web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php new file mode 100644 index 000000000..00234cd9a --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ResolverRelationshipConfigure.php @@ -0,0 +1,182 @@ +get('user.shared_tempstore')); + } + + function __construct(SharedTempStoreFactory $tempstore) { + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_context_configure'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $context = NULL, $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + if (is_numeric($context)) { + $id = $context; + $contexts = $this->getContexts($cached_values); + $context = $contexts[$id]['context']; + $label = $contexts[$id]['label']; + $machine_name = $contexts[$id]['machine_name']; + $description = $contexts[$id]['description']; + // Conditionally set this form element so that we can update or add. + $form['id'] = [ + '#type' => 'value', + '#value' => $id + ]; + } + else { + $label = ''; + $machine_name = ''; + $description = ''; + } + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $form['context'] = [ + '#type' => 'value', + '#value' => $context + ]; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#default_value' => $label, + '#required' => TRUE, + ]; + $form['machine_name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Machine Name'), + '#default_value' => $machine_name, + '#required' => TRUE, + ]; + $form['description'] = [ + '#type' => 'textarea', + '#title' => $this->t('Description'), + '#default_value' => $description, + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#ajax' => [ + 'callback' => [$this, 'ajaxSave'], + ] + ]; + return $form; + } + + public function validateForm(array &$form, FormStateInterface $form_state) { + $machine_name = $form_state->getValue('machine_name'); + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + foreach ($this->getContexts($cached_values) as $id => $context) { + if ($context['machine_name'] == $machine_name) { + $form_state->setError($form['machine_name'], $this->t('That machine name is in use by another context definition.')); + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $contexts = $this->getContexts($cached_values); + $context = [ + 'context' => $form_state->getValue('context'), + 'label' => $form_state->getValue('label'), + 'machine_name' => $form_state->getValue('machine_name'), + 'description' => $form_state->getValue('description'), + ]; + if ($form_state->hasValue('id')) { + $contexts[$form_state->getValue('id')] = $context; + } + else { + $contexts[] = $context; + } + $cached_values = $this->setContexts($cached_values, $contexts); + $this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $form_state->setRedirect($route_name, $route_parameters); + } + + public function ajaxSave(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cached_values); + $response->addCommand(new RedirectCommand($this->url($route_name, $route_parameters))); + $response->addCommand(new CloseModalDialogCommand()); + return $response; + } + + /** + * Document the route name and parameters for redirect after submission. + * + * @param $cached_values + * + * @return array + * In the format of + * return ['route.name', ['machine_name' => $this->machine_name, 'step' => 'step_name]]; + */ + abstract protected function getParentRouteInfo($cached_values); + + /** + * Custom logic for retrieving the contexts array from cached_values. + * + * @param $cached_values + * + * @return array + */ + abstract protected function getContexts($cached_values); + + /** + * Custom logic for setting the contexts array in cached_values. + * + * @param $cached_values + * + * @param $contexts + * The contexts to set within the cached values. + * + * @return mixed + * Return the $cached_values + */ + abstract protected function setContexts($cached_values, $contexts); + +} diff --git a/web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php b/web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php new file mode 100644 index 000000000..4dfde2856 --- /dev/null +++ b/web/modules/contrib/ctools/src/Form/ResolverRelationshipDelete.php @@ -0,0 +1,150 @@ +get('user.shared_tempstore'), $container->get('ctools.typed_data.resolver')); + } + + /** + * @param \Drupal\user\SharedTempStoreFactory $tempstore + * The shared tempstore. + * @param \Drupal\ctools\TypedDataResolver $resolver + * The the typed data resolver. + */ + public function __construct(SharedTempStoreFactory $tempstore, TypedDataResolver $resolver) { + $this->tempstore = $tempstore; + $this->resolver = $resolver; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_resolver_relationship_delete'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion($id = NULL, $cached_values = []) { + $context = $this->getContexts($cached_values)[$id]; + return $this->t('Are you sure you want to delete the @label relationship?', [ + '@label' => $context->getContextDefinition()->getLabel(), + ]); + } + + /** + * {@inheritdoc} + */ + abstract public function getCancelUrl($cached_values = []); + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $this->id = $id; + + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $form ['#title'] = $this->getQuestion($id, $cached_values); + + $form ['#attributes']['class'][] = 'confirmation'; + $form ['description'] = array('#markup' => $this->getDescription()); + $form [$this->getFormName()] = array('#type' => 'hidden', '#value' => 1); + + // By default, render the form using theme_confirm_form(). + if (!isset($form ['#theme'])) { + $form ['#theme'] = 'confirm_form'; + } + $form['actions'] = array('#type' => 'actions'); + $form['actions'] += $this->actions($form, $form_state, $cached_values); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + $form_state->setRedirectUrl($this->getCancelUrl($cached_values)); + } + + /** + * A custom form actions method. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * @param $cached_values + * The current wizard cached values. + * + * @return array + */ + protected function actions(array $form, FormStateInterface $form_state, $cached_values) { + return array( + 'submit' => array( + '#type' => 'submit', + '#value' => $this->getConfirmText(), + '#validate' => array( + array($this, 'validate'), + ), + '#submit' => array( + array($this, 'submitForm'), + ), + ), + 'cancel' => ConfirmFormHelper::buildCancelLink($this, $this->getRequest()), + ); + } + + /** + * Extract contexts from the cached values. + * + * @param array $cached_values + * The cached values. + * + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + */ + abstract public function getContexts($cached_values); + +} diff --git a/web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php b/web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php new file mode 100644 index 000000000..6458d90e2 --- /dev/null +++ b/web/modules/contrib/ctools/src/ParamConverter/TempstoreConverter.php @@ -0,0 +1,169 @@ +tempstore = $tempstore; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function convert($value, $definition, $name, array $defaults) { + $tempstore_id = !empty($definition['tempstore_id']) ? $definition['tempstore_id'] : $defaults['tempstore_id']; + $machine_name = $this->convertVariable($value, $defaults); + + list(, $parts) = explode(':', $definition['type'], 2); + $parts = explode(':', $parts); + foreach ($parts as $key => $part) { + $parts[$key] = $this->convertVariable($part, $defaults); + } + $cached_values = $this->tempstore->get($tempstore_id)->get($machine_name); + // Entity type upcasting is most common, so we just assume that here. + // @todo see if there's a better way to do this. + if (!$cached_values && $this->entityTypeManager->hasDefinition($name)) { + $value = $this->entityTypeManager->getStorage($name)->load($machine_name); + return $value; + } + elseif (!$cached_values) { + return NULL; + } + else { + $value = NestedArray::getValue($cached_values, $parts, $key_exists); + return $key_exists ? $value : NULL; + } + } + + /** + * A helper function for converting string variable names from the defaults. + * + * @param mixed $name + * If name is a string in the format of {var} it will parse the defaults + * for a 'var' default. If $name isn't a string or isn't a slug, it will + * return the raw $name value. If no default is found, it will return NULL + * @param array $defaults + * The route defaults array. + * + * @return mixed + * The value of a variable in defaults. + */ + protected function convertVariable($name, $defaults) { + if (is_string($name) && strpos($name, '{') === 0) { + $length = strlen($name); + $name = substr($name, 1, $length -2); + return isset($defaults[$name]) ? $defaults[$name] : NULL; + } + return $name; + } + + /** + * {@inheritdoc} + */ + public function applies($definition, $name, Route $route) { + if (!empty($definition['type']) && ($definition['type'] == 'tempstore' || strpos($definition['type'], 'tempstore:') === 0)) { + if (!empty($definition['tempstore_id']) || $route->hasDefault('tempstore_id')) { + return TRUE; + } + } + return FALSE; + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Block/EntityView.php b/web/modules/contrib/ctools/src/Plugin/Block/EntityView.php new file mode 100644 index 000000000..51342d010 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Block/EntityView.php @@ -0,0 +1,105 @@ +entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'view_mode' => 'default', + ]; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $form['view_mode'] = [ + '#type' => 'select', + '#options' => $this->entityManager->getViewModeOptions($this->getDerivativeId()), + '#title' => $this->t('View mode'), + '#default_value' => $this->configuration['view_mode'], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['view_mode'] = $form_state->getValue('view_mode'); + } + + /** + * {@inheritdoc} + */ + public function build() { + /** @var $entity \Drupal\Core\Entity\EntityInterface */ + $entity = $this->getContextValue('entity'); + + $view_builder = $this->entityManager->getViewBuilder($entity->getEntityTypeId()); + $build = $view_builder->view($entity, $this->configuration['view_mode']); + + CacheableMetadata::createFromObject($this->getContext('entity')) + ->applyTo($build); + + return $build; + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php b/web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php new file mode 100644 index 000000000..56d51b7cf --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/BlockPluginCollection.php @@ -0,0 +1,53 @@ + $block) { + $configuration = $block->getConfiguration(); + $region = isset($configuration['region']) ? $configuration['region'] : NULL; + $region_assignments[$region][$block_id] = $block; + } + foreach ($region_assignments as $region => $region_assignment) { + // @todo Determine the reason this needs error suppression. + @uasort($region_assignment, function (BlockPluginInterface $a, BlockPluginInterface $b) { + $a_config = $a->getConfiguration(); + $a_weight = isset($a_config['weight']) ? $a_config['weight'] : 0; + $b_config = $b->getConfiguration(); + $b_weight = isset($b_config['weight']) ? $b_config['weight'] : 0; + if ($a_weight == $b_weight) { + return strcmp($a->label(), $b->label()); + } + return $a_weight > $b_weight ? 1 : -1; + }); + $region_assignments[$region] = $region_assignment; + } + return $region_assignments; + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/BlockVariantInterface.php b/web/modules/contrib/ctools/src/Plugin/BlockVariantInterface.php new file mode 100644 index 000000000..aa2709ace --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/BlockVariantInterface.php @@ -0,0 +1,96 @@ +getBlockCollection()->get($block_id); + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::addBlock() + */ + public function addBlock(array $configuration) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + $this->getBlockCollection()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::removeBlock() + */ + public function removeBlock($block_id) { + $this->getBlockCollection()->removeInstanceId($block_id); + return $this; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::updateBlock() + */ + public function updateBlock($block_id, array $configuration) { + $existing_configuration = $this->getBlock($block_id)->getConfiguration(); + $this->getBlockCollection()->setInstanceConfiguration($block_id, $configuration + $existing_configuration); + return $this; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignment() + */ + public function getRegionAssignment($block_id) { + $configuration = $this->getBlock($block_id)->getConfiguration(); + return isset($configuration['region']) ? $configuration['region'] : NULL; + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionAssignments() + */ + public function getRegionAssignments() { + // Build an array of the region names in the right order. + $empty = array_fill_keys(array_keys($this->getRegionNames()), []); + $full = $this->getBlockCollection()->getAllByRegion(); + // Merge it with the actual values to maintain the ordering. + return array_intersect_key(array_merge($empty, $full), $empty); + } + + /** + * @see \Drupal\ctools\Plugin\BlockVariantInterface::getRegionName() + */ + public function getRegionName($region) { + $regions = $this->getRegionNames(); + return isset($regions[$region]) ? $regions[$region] : ''; + } + + /** + * Gets the block plugin manager. + * + * @return \Drupal\Core\Block\BlockManager + * The block plugin manager. + */ + protected function getBlockManager() { + if (!$this->blockManager) { + $this->blockManager = \Drupal::service('plugin.manager.block'); + } + return $this->blockManager; + } + + /** + * Returns the block plugins used for this display variant. + * + * @return \Drupal\Core\Block\BlockPluginInterface[]|\Drupal\ctools\Plugin\BlockPluginCollection + * An array or collection of configured block plugins. + */ + protected function getBlockCollection() { + if (!$this->blockPluginCollection) { + $this->blockPluginCollection = new BlockPluginCollection($this->getBlockManager(), $this->getBlockConfig()); + } + return $this->blockPluginCollection; + } + + /** + * Returns the UUID generator. + * + * @return \Drupal\Component\Uuid\UuidInterface + */ + abstract protected function uuidGenerator(); + + /** + * Returns the configuration for stored blocks. + * + * @return array + * An array of block configuration, keyed by the unique block ID. + */ + abstract protected function getBlockConfig(); + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Condition/EntityBundle.php b/web/modules/contrib/ctools/src/Plugin/Condition/EntityBundle.php new file mode 100644 index 000000000..57547f348 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Condition/EntityBundle.php @@ -0,0 +1,160 @@ +entityTypeBundleInfo = $entity_type_bundle_info; + $this->bundleOf = $entity_type_manager->getDefinition($this->getDerivativeId()); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info'), + $configuration, + $plugin_id, + $plugin_definition + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $options = array(); + $bundles = $this->entityTypeBundleInfo->getBundleInfo($this->bundleOf->id()); + foreach ($bundles as $id => $info) { + $options[$id] = $info['label']; + } + $form['bundles'] = array( + '#title' => $this->pluginDefinition['label'], + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => $this->configuration['bundles'], + ); + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['bundles'] = array_filter($form_state->getValue('bundles')); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + if (empty($this->configuration['bundles']) && !$this->isNegated()) { + return TRUE; + } + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $this->getContextValue($this->bundleOf->id()); + return !empty($this->configuration['bundles'][$entity->bundle()]); + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (count($this->configuration['bundles']) > 1) { + $bundles = $this->configuration['bundles']; + $last = array_pop($bundles); + $bundles = implode(', ', $bundles); + return $this->t('@bundle_type is @bundles or @last', array('@bundle_type' => $this->bundleOf->getBundleLabel(), '@bundles' => $bundles, '@last' => $last)); + } + $bundle = reset($this->configuration['bundles']); + return $this->t('@bundle_type is @bundle', array('@bundle_type' => $this->bundleOf->getBundleLabel(), '@bundle' => $bundle)); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array('bundles' => array()) + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + * + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + */ + public function applyConstraints(array $contexts = array()) { + // Nullify any bundle constraints on contexts we care about. + $this->removeConstraints($contexts); + $bundle = array_values($this->configuration['bundles']); + // There's only one expected context for this plugint type. + foreach ($this->getContextMapping() as $definition_id => $context_id) { + $contexts[$context_id]->getContextDefinition()->addConstraint('Bundle', ['value' => $bundle]); + } + } + + /** + * {@inheritdoc} + * + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + */ + public function removeConstraints(array $contexts = array()) { + // Reset the bundle constraint for any context we've mapped. + foreach ($this->getContextMapping() as $definition_id => $context_id) { + $constraints = $contexts[$context_id]->getContextDefinition()->getConstraints(); + unset($constraints['Bundle']); + $contexts[$context_id]->getContextDefinition()->setConstraints($constraints); + } + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php b/web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php new file mode 100644 index 000000000..3cf96006a --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Condition/NodeType.php @@ -0,0 +1,41 @@ +removeConstraints($contexts); + // If a single bundle is configured, we can set a proper constraint. + if (count($this->configuration['bundles']) == 1) { + $bundle = array_values($this->configuration['bundles']); + foreach ($this->getContextMapping() as $definition_id => $context_id) { + $contexts[$context_id]->getContextDefinition()->addConstraint('Bundle', ['value' => $bundle[0]]); + } + } + } + + /** + * {@inheritdoc} + * + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + */ + public function removeConstraints(array $contexts = array()) { + // Reset the bundle constraint for any context we've mapped. + foreach ($this->getContextMapping() as $definition_id => $context_id) { + $constraints = $contexts[$context_id]->getContextDefinition()->getConstraints(); + unset($constraints['Bundle']); + $contexts[$context_id]->getContextDefinition()->setConstraints($constraints); + } + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php b/web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php new file mode 100644 index 000000000..073ab4a89 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Deriver/EntityBundle.php @@ -0,0 +1,53 @@ +entityManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->hasKey('bundle')) { + $this->derivatives[$entity_type_id] = $base_plugin_definition; + $this->derivatives[$entity_type_id]['label'] = $this->getEntityBundleLabel($entity_type); + $this->derivatives[$entity_type_id]['context'] = [ + "$entity_type_id" => new ContextDefinition('entity:' . $entity_type_id), + ]; + } + } + return $this->derivatives; + } + + /** + * Provides the bundle label with a fallback when not defined. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type we are looking the bundle label for. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The entity bundle label or a fallback label. + */ + protected function getEntityBundleLabel($entity_type) { + + if ($label = $entity_type->getBundleLabel()) { + return $this->t('@label', ['@label' => $label]); + } + + $fallback = $entity_type->getLabel(); + if ($bundle_entity_type = $entity_type->getBundleEntityType()) { + // This is a better fallback. + $fallback = $this->entityManager->getDefinition($bundle_entity_type)->getLabel(); + } + + return $this->t('@label bundle', ['@label' => $fallback]); + + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Deriver/EntityDeriverBase.php b/web/modules/contrib/ctools/src/Plugin/Deriver/EntityDeriverBase.php new file mode 100644 index 000000000..1adec9965 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Deriver/EntityDeriverBase.php @@ -0,0 +1,51 @@ +entityManager = $entity_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity.manager'), + $container->get('string_translation') + ); + } + +} + diff --git a/web/modules/contrib/ctools/src/Plugin/Deriver/EntityViewDeriver.php b/web/modules/contrib/ctools/src/Plugin/Deriver/EntityViewDeriver.php new file mode 100644 index 000000000..f8f189c5f --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Deriver/EntityViewDeriver.php @@ -0,0 +1,28 @@ +entityManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->hasViewBuilderClass()) { + $this->derivatives[$entity_type_id] = $base_plugin_definition; + $this->derivatives[$entity_type_id]['admin_label'] = $this->t('Entity view (@label)', ['@label' => $entity_type->getLabel()]); + $this->derivatives[$entity_type_id]['context'] = [ + 'entity' => new ContextDefinition('entity:' . $entity_type_id), + ]; + } + } + return $this->derivatives; + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php new file mode 100644 index 000000000..cd2d83fef --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataEntityRelationshipDeriver.php @@ -0,0 +1,24 @@ +getType() == 'entity_reference') { + parent::generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition); + } + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php new file mode 100644 index 000000000..d662d2985 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataLanguageRelationshipDeriver.php @@ -0,0 +1,40 @@ +getType() == 'language') { + parent::generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition); + } + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + parent::getDerivativeDefinitions($base_plugin_definition); + // The data types will all be set to string since language extends string + // and the parent class finds the related primitive. + foreach ($this->derivatives as $plugin_id => $derivative) { + $this->derivatives[$plugin_id]['data_type'] = 'language'; + } + return $this->derivatives; + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php new file mode 100644 index 000000000..ae8bbc2d0 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataPropertyDeriverBase.php @@ -0,0 +1,113 @@ +typedDataManager = $typed_data_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('typed_data_manager'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + foreach ($this->typedDataManager->getDefinitions() as $data_type_id => $data_type_definition) { + if (is_subclass_of($data_type_definition['class'], ComplexDataInterface::class, TRUE)) { + /** @var \Drupal\Core\TypedData\ComplexDataDefinitionInterface $base_definition */ + $base_definition = $this->typedDataManager->createDataDefinition($data_type_id); + foreach ($base_definition->getPropertyDefinitions() as $property_name => $property_definition) { + if ($property_definition instanceof BaseFieldDefinition || $property_definition instanceof FieldConfig) { + $this->generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition); + } + } + } + } + return $this->derivatives; + } + + /** + * @param $property_definition + * + * @return mixed + */ + protected function getDataType($property_definition) { + if ($property_definition instanceof DataReferenceDefinitionInterface) { + return $property_definition->getTargetDefinition()->getDataType(); + } + if ($property_definition instanceof ListDataDefinitionInterface) { + return $property_definition->getItemDefinition()->getDataType(); + } + return $property_definition->getDataType(); + } + + /** + * Generates and maintains a derivative definition. + * + * This method should directly manipulate $this->derivatives and not return + * values. This allows implementations control over the derivative names. + * + * @param $base_plugin_definition + * The base plugin definition. + * @param string $data_type_id + * The plugin id of the data type. + * @param mixed $data_type_definition + * The plugin definition of the data type. + * @param \Drupal\Core\TypedData\DataDefinitionInterface $base_definition + * The data type definition of a complex data object. + * @param string $property_name + * The name of the property + * @param \Drupal\Core\TypedData\DataDefinitionInterface $property_definition + * The property definition. + * + */ + abstract protected function generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, DataDefinitionInterface $base_definition, $property_name, DataDefinitionInterface $property_definition); + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php new file mode 100644 index 000000000..e0f8dbbd8 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Deriver/TypedDataRelationshipDeriver.php @@ -0,0 +1,75 @@ +getConstraint('Bundle'); + // Identify base definitions that appear on bundle-able entities. + if ($bundle_info && array_filter($bundle_info) && $base_definition->getConstraint('EntityType')) { + $base_data_type = 'entity:' . $base_definition->getConstraint('EntityType'); + } + // Otherwise, just use the raw data type identifier. + else { + $base_data_type = $data_type_id; + } + // If we've not processed this thing before. + if (!isset($this->derivatives[$base_data_type . ':' . $property_name])) { + $derivative = $base_plugin_definition; + + $derivative['label'] = $this->t($this->label, [ + '@property' => $property_definition->getLabel(), + '@base' => $data_type_definition['label'], + ]); + $derivative['data_type'] = $property_definition->getFieldStorageDefinition()->getPropertyDefinition($property_definition->getFieldStorageDefinition()->getMainPropertyName())->getDataType(); + $derivative['property_name'] = $property_name; + $context_definition = new ContextDefinition($base_data_type, $this->typedDataManager->createDataDefinition($base_data_type)); + // Add the constraints of the base definition to the context definition. + if ($base_definition->getConstraint('Bundle')) { + $context_definition->addConstraint('Bundle', $base_definition->getConstraint('Bundle')); + } + $derivative['context'] = [ + 'base' => $context_definition, + ]; + $derivative['property_name'] = $property_name; + + $this->derivatives[$base_data_type . ':' . $property_name] = $derivative; + } + // Individual fields can be on multiple bundles. + elseif ($property_definition instanceof FieldConfigInterface) { + // We should only end up in here on entity bundles. + $derivative = $this->derivatives[$base_data_type . ':' . $property_name]; + // Update label + /** @var \Drupal\Core\StringTranslation\TranslatableMarkup $label */ + $label = $derivative['label']; + list(,, $argument_name) = explode(':', $data_type_id); + $arguments = $label->getArguments(); + $arguments['@'. $argument_name] = $data_type_definition['label']; + $string_args = $arguments; + array_shift($string_args); + $last = array_slice($string_args, -1); + // The slice doesn't remove, so do that now. + array_pop($string_args); + $string = count($string_args) >= 2 ? '@property from '. implode(', ', array_keys($string_args)) .' and '. array_keys($last)[0] : '@property from @base and '. array_keys($last)[0]; + $this->derivatives[$base_data_type . ':' . $property_name]['label'] = $this->t($string, $arguments); + if ($base_definition->getConstraint('Bundle')) { + // Add bundle constraints + $context_definition = $derivative['context']['base']; + $bundles = $context_definition->getConstraint('Bundle') ?: []; + $bundles = array_merge($bundles, $base_definition->getConstraint('Bundle')); + $context_definition->addConstraint('Bundle', $bundles); + } + } + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/DisplayVariant/BlockDisplayVariant.php b/web/modules/contrib/ctools/src/Plugin/DisplayVariant/BlockDisplayVariant.php new file mode 100644 index 000000000..32c6e86fd --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/DisplayVariant/BlockDisplayVariant.php @@ -0,0 +1,226 @@ +contextHandler = $context_handler; + $this->account = $account; + $this->uuidGenerator = $uuid_generator; + $this->token = $token; + $this->blockManager = $block_manager; + $this->conditionManager = $condition_manager; + + 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('context.handler'), + $container->get('current_user'), + $container->get('uuid'), + $container->get('token'), + $container->get('plugin.manager.block'), + $container->get('plugin.manager.condition') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return parent::defaultConfiguration() + [ + 'blocks' => [] + ]; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + foreach ($this->getBlockCollection() as $instance) { + $this->calculatePluginDependencies($instance); + } + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return [ + 'blocks' => $this->getBlockCollection()->getConfiguration(), + ] + parent::getConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + // preserve the uuid. + if ($this->configuration && !empty($this->configuration['uuid'])) { + $configuration['uuid'] = $this->configuration['uuid']; + } + parent::setConfiguration($configuration); + $this->getBlockCollection()->setConfiguration($this->configuration['blocks']); + return $this; + } + + /** + * Gets the contexts. + * + * @return \Drupal\Component\Plugin\Context\ContextInterface[] + * An array of set contexts, keyed by context name. + */ + public function getContexts() { + return $this->contexts; + } + + /** + * Sets the contexts. + * + * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts + * An array of contexts, keyed by context name. + * + * @return $this + */ + public function setContexts(array $contexts) { + $this->contexts = $contexts; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function contextHandler() { + return $this->contextHandler; + } + + /** + * {@inheritdoc} + */ + protected function getBlockConfig() { + return $this->configuration['blocks']; + } + + /** + * {@inheritdoc} + */ + protected function uuidGenerator() { + return $this->uuidGenerator; + } + + /** + * {@inheritdoc} + */ + public function __sleep() { + $vars = parent::__sleep(); + + // Gathered contexts objects should not be serialized. + if (($key = array_search('contexts', $vars)) !== FALSE) { + unset($vars[$key]); + } + + // The block plugin collection should also not be serialized, ensure that + // configuration is synced back. + if (($key = array_search('blockPluginCollection', $vars)) !== FALSE) { + if ($this->blockPluginCollection) { + $this->configuration['blocks'] = $this->blockPluginCollection->getConfiguration(); + } + unset($vars[$key]); + } + + return $vars; + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/PluginWizardInterface.php b/web/modules/contrib/ctools/src/Plugin/PluginWizardInterface.php new file mode 100644 index 000000000..cbb5e7714 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/PluginWizardInterface.php @@ -0,0 +1,25 @@ +getPluginDefinition(); + + $entity_type = $this->getData($this->getContext('base'))->getDataDefinition()->getSetting('target_type'); + $context_definition = new ContextDefinition("entity:$entity_type", $plugin_definition['label']); + $context_value = NULL; + + // If the 'base' context has a value, then get the property value to put on + // the context (otherwise, mapping hasn't occurred yet and we just want to + // return the context with the right definition and no value). + if ($this->getContext('base')->hasContextValue()) { + $context_value = $this->getData($this->getContext('base'))->entity; + } + + $context_definition->setDefaultValue($context_value); + return new Context($context_definition, $context_value); + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataLanguageRelationship.php b/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataLanguageRelationship.php new file mode 100644 index 000000000..c19823180 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataLanguageRelationship.php @@ -0,0 +1,35 @@ +getPluginDefinition(); + + $context_definition = new ContextDefinition("language", $plugin_definition['label']); + $context_value = NULL; + + // If the 'base' context has a value, then get the property value to put on + // the context (otherwise, mapping hasn't occurred yet and we just want to + // return the context with the right definition and no value). + if ($this->getContext('base')->hasContextValue()) { + $context_value = $this->getData($this->getContext('base'))->language; + } + + $context_definition->setDefaultValue($context_value); + return new Context($context_definition, $context_value); + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php b/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php new file mode 100644 index 000000000..d795f28ca --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/Relationship/TypedDataRelationship.php @@ -0,0 +1,77 @@ +getPluginDefinition(); + + $data_type = $plugin_definition['data_type']; + $context_definition = new ContextDefinition($data_type, $plugin_definition['label']); + $context_value = NULL; + + // If the 'base' context has a value, then get the property value to put on + // the context (otherwise, mapping hasn't occurred yet and we just want to + // return the context with the right definition and no value). + if ($this->getContext('base')->hasContextValue()) { + $data = $this->getData($this->getContext('base')); + $property = $this->getMainPropertyName($data); + $context_value = $data->get($property)->getValue(); + } + + $context_definition->setDefaultValue($context_value); + return new Context($context_definition, $context_value); + } + + public function getName() { + return $this->getPluginDefinition()['property_name']; + } + + protected function getData(ContextInterface $context) { + /** @var \Drupal\Core\TypedData\ComplexDataInterface $base */ + $base = $context->getContextValue(); + $name = $this->getPluginDefinition()['property_name']; + $data = $base->get($name); + // @todo add configuration to get N instead of first. + if ($data instanceof ListInterface) { + $data = $data->first(); + } + if ($data instanceof DataReferenceInterface) { + $data = $data->getTarget(); + } + return $data; + } + + protected function getMainPropertyName(FieldItemInterface $data) { + return $data->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); + } + + public function getRelationshipValue() { + $property = $this->getMainPropertyName(); + /** @var \Drupal\Core\TypedData\ComplexDataInterface $data */ + $data = $this->getRelationship()->getContextData(); + $data->get($property)->getValue(); + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/RelationshipBase.php b/web/modules/contrib/ctools/src/Plugin/RelationshipBase.php new file mode 100644 index 000000000..9f1e1cf18 --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/RelationshipBase.php @@ -0,0 +1,10 @@ +alterInfo('ctools_relationship_info'); + $this->setCacheBackend($cache_backend, 'ctools_relationship_plugins'); + } + +} diff --git a/web/modules/contrib/ctools/src/Plugin/RelationshipManagerInterface.php b/web/modules/contrib/ctools/src/Plugin/RelationshipManagerInterface.php new file mode 100644 index 000000000..a97fa1ffa --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/RelationshipManagerInterface.php @@ -0,0 +1,10 @@ +uuidGenerator()->generate(); + $this->getVariants()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * @see \Drupal\ctools\Plugin\VariantCollectionInterface::getVariant() + */ + public function getVariant($variant_id) { + return $this->getVariants()->get($variant_id); + } + + /** + * @see \Drupal\ctools\Plugin\VariantCollectionInterface::removeVariant() + */ + public function removeVariant($variant_id) { + $this->getVariants()->removeInstanceId($variant_id); + return $this; + } + + /** + * @see \Drupal\ctools\Plugin\VariantCollectionInterface::getVariants() + */ + public function getVariants() { + if (!$this->variantCollection) { + $this->variantCollection = new VariantPluginCollection(\Drupal::service('plugin.manager.display_variant'), $this->getVariantConfig()); + $this->variantCollection->sort(); + } + return $this->variantCollection; + } + + /** + * Returns the configuration for stored variants. + * + * @return array + * An array of variant configuration, keyed by the unique variant ID. + */ + abstract protected function getVariantConfig(); + + /** + * Returns the UUID generator. + * + * @return \Drupal\Component\Uuid\UuidInterface + */ + abstract protected function uuidGenerator(); + +} diff --git a/web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php b/web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php new file mode 100644 index 000000000..77914cd1d --- /dev/null +++ b/web/modules/contrib/ctools/src/Plugin/VariantPluginCollection.php @@ -0,0 +1,43 @@ +instanceIDs, [$this, 'sortHelper']); + return $this; + } + + /** + * {@inheritdoc} + */ + public function sortHelper($aID, $bID) { + $a_weight = $this->get($aID)->getWeight(); + $b_weight = $this->get($bID)->getWeight(); + if ($a_weight == $b_weight) { + return strcmp($aID, $bID); + } + + return ($a_weight < $b_weight) ? -1 : 1; + } + +} diff --git a/web/modules/contrib/ctools/src/Routing/Enhancer/WizardEnhancer.php b/web/modules/contrib/ctools/src/Routing/Enhancer/WizardEnhancer.php new file mode 100644 index 000000000..3e125eb9e --- /dev/null +++ b/web/modules/contrib/ctools/src/Routing/Enhancer/WizardEnhancer.php @@ -0,0 +1,34 @@ +hasDefault('_controller') && ($route->hasDefault('_wizard') || $route->hasDefault('_entity_wizard')); + } + + /** + * {@inheritdoc} + */ + public function enhance(array $defaults, Request $request) { + if (!empty($defaults['_wizard'])) { + $defaults['_controller'] = 'ctools.wizard.form:getContentResult'; + } + if (!empty($defaults['_entity_wizard'])) { + $defaults['_controller'] = 'ctools.wizard.entity.form:getContentResult'; + } + return $defaults; + } + +} diff --git a/web/modules/contrib/ctools/src/SerializableTempstore.php b/web/modules/contrib/ctools/src/SerializableTempstore.php new file mode 100644 index 000000000..65567edc5 --- /dev/null +++ b/web/modules/contrib/ctools/src/SerializableTempstore.php @@ -0,0 +1,13 @@ +id() ?: session_id(); + } + + // Store the data for this collection in the database. + $storage = $this->storageFactory->get("user.shared_tempstore.$collection"); + return new SerializableTempstore($storage, $this->lockBackend, $owner, $this->requestStack, $this->expire); + } + +} diff --git a/web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php b/web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php new file mode 100644 index 000000000..358f8a17a --- /dev/null +++ b/web/modules/contrib/ctools/src/Testing/EntityCreationTrait.php @@ -0,0 +1,55 @@ + 'foo'. + * + * @return \Drupal\Core\Entity\EntityInterface + * Created entity. + */ + protected function createEntity($entity_type, array $values = array()) { + $storage = $this->getEntityTypeManager()->getStorage($entity_type); + $entity = $storage->create($values); + $status = $entity->save(); + \Drupal::service('router.builder')->rebuild(); + + if ($this instanceof \PHPUnit_Framework_TestCase) { + $this->assertSame($status, SAVED_NEW, (new FormattableMarkup('Created entity %id of type %type.', ['%id' => $entity->id(), '%type' => $entity_type]))->__toString()); + } + else { + $this->assertEqual($status, SAVED_NEW, (new FormattableMarkup('Created entity %id of type %type.', ['%id' => $entity->id(), '%type' => $entity_type]))->__toString()); + } + + return $entity; + } + + /** + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected function getEntityTypeManager() { + if (!isset($this->entityTypeManager)) { + $this->entityTypeManager = $this->container->get('entity_type.manager'); + } + return $this->entityTypeManager; + } + +} diff --git a/web/modules/contrib/ctools/src/Tests/Wizard/CToolsWizardTest.php b/web/modules/contrib/ctools/src/Tests/Wizard/CToolsWizardTest.php new file mode 100644 index 000000000..c9b91eada --- /dev/null +++ b/web/modules/contrib/ctools/src/Tests/Wizard/CToolsWizardTest.php @@ -0,0 +1,136 @@ +drupalGet('ctools/wizard'); + $this->assertText('Form One'); + $this->dumpHeaders = TRUE; + // Check that $operations['one']['values'] worked. + $this->assertText('Xylophone'); + // Submit first step in the wizard. + $edit = [ + 'one' => 'test', + ]; + $this->drupalPostForm('ctools/wizard', $edit, $this->t('Next')); + // Redirected to the second step. + $this->assertText('Form Two'); + $this->assertText('Dynamic value submitted: Xylophone'); + // Check that $operations['two']['values'] worked. + $this->assertText('Zebra'); + // Hit previous to make sure our form value are preserved. + $this->drupalPostForm(NULL, [], $this->t('Previous')); + // Check the known form values. + $this->assertFieldByName('one', 'test'); + $this->assertText('Xylophone'); + // Goto next step again and finish this wizard. + $this->drupalPostForm(NULL, [], $this->t('Next')); + $edit = [ + 'two' => 'Second test', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Finish')); + // Check that the wizard finished properly. + $this->assertText('Value One: test'); + $this->assertText('Value Two: Second test'); + } + + function testStepValidateAndSubmit() { + $this->drupalGet('ctools/wizard'); + $this->assertText('Form One'); + // Submit first step in the wizard. + $edit = [ + 'one' => 'wrong', + ]; + $this->drupalPostForm('ctools/wizard', $edit, $this->t('Next')); + // We're still on the first form and the error is present. + $this->assertText('Form One'); + $this->assertText('Cannot set the value to "wrong".'); + // Try again with the magic value. + $edit = [ + 'one' => 'magic', + ]; + $this->drupalPostForm('ctools/wizard', $edit, $this->t('Next')); + // Redirected to the second step. + $this->assertText('Form Two'); + $edit = [ + 'two' => 'Second test', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Finish')); + // Check that the magic value triggered our submit callback. + $this->assertText('Value One: Abraham'); + $this->assertText('Value Two: Second test'); + } + + function testEntityWizard() { + $this->drupalLogin($this->drupalCreateUser(['administer site configuration'])); + + // Start adding a new config entity. + $this->drupalGet('admin/structure/ctools_wizard_test_config_entity/add'); + $this->assertText('Example entity'); + $this->assertNoText('Existing entity'); + + // Submit the general step. + $edit = [ + 'id' => 'test123', + 'label' => 'Test Config Entity 123', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Next')); + + // Submit the first step. + $edit = [ + 'one' => 'The first bit', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Next')); + + // Submit the second step. + $edit = [ + 'two' => 'The second bit', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Finish')); + + // Now we should be looking at the list of entities. + $this->assertUrl('admin/structure/ctools_wizard_test_config_entity'); + $this->assertText('Test Config Entity 123'); + + // Edit the entity again and make sure the values are what we expect. + $this->clickLink(t('Edit')); + $this->assertText('Existing entity'); + $this->assertFieldByName('label', 'Test Config Entity 123'); + $this->clickLink(t('Form One')); + $this->assertFieldByName('one', 'The first bit'); + $previous = $this->getUrl(); + $this->clickLink(t('Show on dialog')); + $this->assertRaw('Value from one: The first bit'); + $this->drupalGet($previous); + // Change the value for 'one'. + $this->drupalPostForm(NULL, ['one' => 'New value'], $this->t('Next')); + $this->assertFieldByName('two', 'The second bit'); + $this->drupalPostForm(NULL, [], $this->t('Next')); + // Make sure we get the additional step because the entity exists. + $this->assertText('This step only shows if the entity is already existing!'); + $this->drupalPostForm(NULL, [], $this->t('Finish')); + + // Edit the entity again and make sure the change stuck. + $this->assertUrl('admin/structure/ctools_wizard_test_config_entity'); + $this->clickLink(t('Edit')); + $this->drupalPostForm(NULL, [], $this->t('Next')); + $this->assertFieldByName('one', 'New value'); + } + +} + diff --git a/web/modules/contrib/ctools/src/TypedDataResolver.php b/web/modules/contrib/ctools/src/TypedDataResolver.php new file mode 100644 index 000000000..737ab3ce1 --- /dev/null +++ b/web/modules/contrib/ctools/src/TypedDataResolver.php @@ -0,0 +1,253 @@ +manager = $manager; + $this->translation = $translation; + } + + /** + * Convert a property to a context. + * + * This method will respect the value of contexts as well, so if a context + * object is pass that contains a value, the appropriate value will be + * extracted and injected into the resulting context object if available. + * + * @param string $property_path + * The name of the property. + * @param \Drupal\Core\Plugin\Context\ContextInterface $context + * The context from which we will extract values if available. + * + * @return \Drupal\Core\Plugin\Context\Context + * A context object that represents the definition & value of the property. + * @throws \Exception + */ + public function getContextFromProperty($property_path, ContextInterface $context) { + $value = NULL; + $data_definition = NULL; + if ($context->hasContextValue()) { + /** @var \Drupal\Core\TypedData\ComplexDataInterface $data */ + $data = $context->getContextData(); + foreach (explode(':', $property_path) as $name) { + + if ($data instanceof ListInterface) { + if (!is_numeric($name)) { + // Implicitly default to delta 0 for lists when not specified. + $data = $data->first(); + } + else { + // If we have a delta, fetch it and continue with the next part. + $data = $data->get($name); + continue; + } + } + + // Forward to the target value if this is a data reference. + if ($data instanceof DataReferenceInterface) { + $data = $data->getTarget(); + } + + if (!$data->getDataDefinition()->getPropertyDefinition($name)) { + throw new \Exception("Unknown property $name in property path $property_path"); + } + $data = $data->get($name); + } + + $value = $data->getValue(); + $data_definition = $data instanceof DataReferenceInterface ? $data->getDataDefinition()->getTargetDefinition() : $data->getDataDefinition(); + } + else { + /** @var \Drupal\Core\TypedData\ComplexDataDefinitionInterface $data_definition */ + $data_definition = $context->getContextDefinition()->getDataDefinition(); + foreach (explode(':', $property_path) as $name) { + + if ($data_definition instanceof ListDataDefinitionInterface) { + $data_definition = $data_definition->getItemDefinition(); + + // If the delta was specified explicitly, continue with the next part. + if (is_numeric($name)) { + continue; + } + } + + // Forward to the target definition if this is a data reference + // definition. + if ($data_definition instanceof DataReferenceDefinitionInterface) { + $data_definition = $data_definition->getTargetDefinition(); + } + + if (!$data_definition->getPropertyDefinition($name)) { + throw new \Exception("Unknown property $name in property path $property_path"); + } + $data_definition = $data_definition->getPropertyDefinition($name); + } + + // Forward to the target definition if this is a data reference + // definition. + if ($data_definition instanceof DataReferenceDefinitionInterface) { + $data_definition = $data_definition->getTargetDefinition(); + } + } + $context_definition = new ContextDefinition($data_definition->getDataType(), $data_definition->getLabel(), $data_definition->isRequired(), FALSE, $data_definition->getDescription()); + return new Context($context_definition, $value); + } + + /** + * Extracts a context from an array of contexts by a tokenized pattern. + * + * This is more than simple isset/empty checks on the contexts array. The + * pattern could be node:uid:name which will iterate over all provided + * contexts in the array for one named 'node', it will then load the data + * definition of 'node' and check for a property named 'uid'. This will then + * set a new (temporary) context on the array and recursively call itself to + * navigate through related properties all the way down until the request + * property is located. At that point the property is passed to a + * TypedDataResolver which will convert it to an appropriate ContextInterface + * object. + * + * @param $token + * A ":" delimited set of tokens representing + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * The array of available contexts. + * + * @return \Drupal\Core\Plugin\Context\ContextInterface + * The requested token as a full Context object. + * + * @throws \Drupal\ctools\ContextNotFoundException + */ + public function convertTokenToContext($token, $contexts) { + // If the requested token is already a context, just return it. + if (isset($contexts[$token])) { + return $contexts[$token]; + } + else { + list($base, $property_path) = explode(':', $token, 2); + // A base must always be set. This method recursively calls itself + // setting bases for this reason. + if (!empty($contexts[$base])) { + return $this->getContextFromProperty($property_path, $contexts[$base]); + } + // @todo improve this exception message. + throw new ContextNotFoundException("The requested context was not found in the supplied array of contexts."); + } + } + + /** + * Provides an administrative label for a tokenized relationship. + * + * @param string $token + * The token related to a context in the contexts array. + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * An array of contexts from which to extract our token's label. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The administrative label of $token. + */ + public function getLabelByToken($token, $contexts) { + // @todo Optimize this by allowing to limit the desired token? + $tokens = $this->getTokensForContexts($contexts); + if (isset($tokens[$token])) { + return $tokens[$token]; + } + } + + /** + * Extracts an array of tokens and labels. + * + * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts + * The array of contexts with which we are currently dealing. + * + * @return array + * An array of token keys and corresponding labels. + */ + public function getTokensForContexts($contexts) { + $tokens = []; + foreach ($contexts as $context_id => $context) { + $data_definition = $context->getContextDefinition()->getDataDefinition(); + if ($data_definition instanceof ComplexDataDefinitionInterface) { + foreach ($this->getTokensFromComplexData($data_definition) as $token => $label) { + $tokens["$context_id:$token"] = $data_definition->getLabel() . ': ' . $label; + } + } + } + return $tokens; + } + + /** + * Returns tokens for a complex data definition. + * + * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface $complex_data_definition + * + * @return array + * An array of token keys and corresponding labels. + */ + protected function getTokensFromComplexData(ComplexDataDefinitionInterface $complex_data_definition) { + $tokens = []; + // Loop over all properties. + foreach ($complex_data_definition->getPropertyDefinitions() as $property_name => $property_definition) { + + // Item definitions do not always have a label. Use the list definition + // label if the item does not have one. + $property_label = $property_definition->getLabel(); + if ($property_definition instanceof ListDataDefinitionInterface) { + $property_definition = $property_definition->getItemDefinition(); + $property_label = $property_definition->getLabel() ?: $property_label; + } + + // If the property is complex too, recurse to find child properties. + if ($property_definition instanceof ComplexDataDefinitionInterface) { + $property_tokens = $this->getTokensFromComplexData($property_definition); + foreach ($property_tokens as $token => $label) { + $tokens[$property_name . ':' . $token] = count($property_tokens) > 1 ? ($property_label . ': ' . $label) : $property_label; + } + } + + // Only expose references as tokens. + // @todo Consider to expose primitive and non-reference typed data + // definitions too, like strings, integers and dates. The current UI + // will not scale to that. + if ($property_definition instanceof DataReferenceDefinitionInterface) { + $tokens[$property_name] = $property_definition->getLabel(); + } + } + return $tokens; + } + +} diff --git a/web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php b/web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php new file mode 100644 index 000000000..1647f734a --- /dev/null +++ b/web/modules/contrib/ctools/src/Wizard/EntityFormWizardBase.php @@ -0,0 +1,180 @@ +entityManager = $entity_manager; + parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $tempstore_id, $machine_name, $step); + } + + /** + * {@inheritdoc} + */ + public static function getParameters() { + return [ + 'tempstore' => \Drupal::service('user.shared_tempstore'), + 'builder' => \Drupal::service('form_builder'), + 'class_resolver' => \Drupal::service('class_resolver'), + 'event_dispatcher' => \Drupal::service('event_dispatcher'), + 'entity_manager' => \Drupal::service('entity.manager'), + ]; + } + + /** + * {@inheritdoc} + */ + public function initValues() { + $storage = $this->entityManager->getStorage($this->getEntityType()); + if ($this->getMachineName()) { + $values = $this->getTempstore()->get($this->getMachineName()); + if (!$values) { + $entity = $storage->load($this->getMachineName()); + $values[$this->getEntityType()] = $entity; + $values['id'] = $entity->id(); + $values['label'] = $entity->label(); + } + } + else { + $entity = $storage->create([]); + $values[$this->getEntityType()] = $entity; + } + $event = new WizardEvent($this, $values); + $this->dispatcher->dispatch(FormWizardInterface::LOAD_VALUES, $event); + return $event->getValues(); + } + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $entity \Drupal\Core\Entity\EntityInterface */ + $entity = $cached_values[$this->getEntityType()]; + $entity->set('id', $cached_values['id']); + $entity->set('label', $cached_values['label']); + $status = $entity->save(); + $definition = $this->entityManager->getDefinition($this->getEntityType()); + if ($status) { + drupal_set_message($this->t('Saved the %label @entity_type.', array( + '%label' => $entity->label(), + '@entity_type' => $definition->getLabel(), + ))); + } + else { + drupal_set_message($this->t('The %label @entity_type was not saved.', array( + '%label' => $entity->label(), + '@entity_type' => $definition->getLabel(), + ))); + } + $form_state->setRedirectUrl($entity->toUrl('collection')); + parent::finish($form, $form_state); + } + + /** + * Helper function for generating label and id form elements. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return array + */ + protected function customizeForm(array $form, FormStateInterface $form_state) { + $form = parent::customizeForm($form, $form_state); + if ($this->machine_name) { + $entity = $this->entityManager->getStorage($this->getEntityType()) + ->load($this->machine_name); + } + else { + $entity = NULL; + } + $cached_values = $form_state->getTemporaryValue('wizard'); + // If the entity already exists, allow for non-linear step interaction. + if ($entity) { + // Setup the step rendering theme element. + $prefix = [ + '#theme' => ['ctools_wizard_trail_links'], + '#wizard' => $this, + '#cached_values' => $cached_values, + ]; + $form['#prefix'] = \Drupal::service('renderer')->render($prefix); + } + // Get the current form operation. + $operation = $this->getOperation($cached_values); + $operations = $this->getOperations($cached_values); + $default_operation = reset($operations); + if ($operation['form'] == $default_operation['form']) { + // Get the plugin definition of this entity. + $definition = $this->entityManager->getDefinition($this->getEntityType()); + // Create id and label form elements. + $form['name'] = array( + '#type' => 'fieldset', + '#attributes' => array('class' => array('fieldset-no-legend')), + '#title' => $this->getWizardLabel(), + ); + $form['name']['label'] = array( + '#type' => 'textfield', + '#title' => $this->getMachineLabel(), + '#required' => TRUE, + '#size' => 32, + '#default_value' => !empty($cached_values['label']) ? $cached_values['label'] : '', + '#maxlength' => 255, + '#disabled' => !empty($cached_values['label']), + ); + $form['name']['id'] = array( + '#type' => 'machine_name', + '#maxlength' => 128, + '#machine_name' => array( + 'source' => array('name', 'label'), + 'exists' => $this->exists(), + ), + '#description' => $this->t('A unique machine-readable name for this @entity_type. It must only contain lowercase letters, numbers, and underscores.', ['@entity_type' => $definition->getLabel()]), + '#default_value' => !empty($cached_values['id']) ? $cached_values['id'] : '', + '#disabled' => !empty($cached_values['id']), + ); + } + + return $form; + } + +} diff --git a/web/modules/contrib/ctools/src/Wizard/EntityFormWizardInterface.php b/web/modules/contrib/ctools/src/Wizard/EntityFormWizardInterface.php new file mode 100644 index 000000000..92f557974 --- /dev/null +++ b/web/modules/contrib/ctools/src/Wizard/EntityFormWizardInterface.php @@ -0,0 +1,39 @@ +tempstore = $tempstore; + $this->builder = $builder; + $this->classResolver = $class_resolver; + $this->dispatcher = $event_dispatcher; + $this->routeMatch = $route_match; + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $this->step = $step; + } + + /** + * {@inheritdoc} + */ + public static function getParameters() { + return [ + 'tempstore' => \Drupal::service('user.shared_tempstore'), + 'builder' => \Drupal::service('form_builder'), + 'class_resolver' => \Drupal::service('class_resolver'), + 'event_dispatcher' => \Drupal::service('event_dispatcher'), + ]; + } + + /** + * {@inheritdoc} + */ + public function initValues() { + $values = []; + $event = new WizardEvent($this, $values); + $this->dispatcher->dispatch(FormWizardInterface::LOAD_VALUES, $event); + return $event->getValues(); + } + + /** + * {@inheritdoc} + */ + public function getTempstoreId() { + return $this->tempstore_id; + } + + /** + * {@inheritdoc} + */ + public function getTempstore() { + return $this->tempstore->get($this->getTempstoreId()); + } + + /** + * {@inheritdoc} + */ + public function getMachineName() { + return $this->machine_name; + } + + /** + * {@inheritdoc} + */ + public function getStep($cached_values) { + if (!$this->step) { + $operations = $this->getOperations($cached_values); + $steps = array_keys($operations); + $this->step = reset($steps); + } + return $this->step; + } + + /** + * {@inheritdoc} + */ + public function getOperation($cached_values) { + $operations = $this->getOperations($cached_values); + $step = $this->getStep($cached_values); + if (!empty($operations[$step])) { + return $operations[$step]; + } + $operation = reset($operations); + return $operation; + } + + /** + * The translated text of the "Next" button's text. + * + * @return string + */ + public function getNextOp() { + return $this->t('Next'); + } + + /** + * {@inheritdoc} + */ + public function getNextParameters($cached_values) { + // Get the steps by key. + $operations = $this->getOperations($cached_values); + $steps = array_keys($operations); + // Get the steps after the current step. + $after = array_slice($operations, array_search($this->getStep($cached_values), $steps) + 1); + // Get the steps after the current step by key. + $after_keys = array_keys($after); + $step = reset($after_keys); + if (!$step) { + $keys = array_keys($operations); + $step = end($keys); + } + return [ + 'machine_name' => $this->getMachineName(), + 'step' => $step, + 'js' => 'nojs', + ]; + } + + /** + * {@inheritdoc} + */ + public function getPreviousParameters($cached_values) { + $operations = $this->getOperations($cached_values); + $step = $this->getStep($cached_values); + + // Get the steps by key. + $steps = array_keys($operations); + // Get the steps before the current step. + $before = array_slice($operations, 0, array_search($step, $steps)); + // Get the steps before the current step by key. + $before = array_keys($before); + // Reverse the steps for easy access to the next step. + $before_steps = array_reverse($before); + $step = reset($before_steps); + return [ + 'machine_name' => $this->getMachineName(), + 'step' => $step, + 'js' => 'nojs', + ]; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + if (!$this->getMachineName() || !$this->getTempstore()->get($this->getMachineName())) { + $cached_values = $this->initValues(); + } + else { + $cached_values = $this->getTempstore()->get($this->getMachineName()); + } + $operation = $this->getOperation($cached_values); + /* @var $operation \Drupal\Core\Form\FormInterface */ + $operation = $this->classResolver->getInstanceFromDefinition($operation['form']); + return $operation->getFormId(); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + // Get the current form operation. + $operation = $this->getOperation($cached_values); + $form = $this->customizeForm($form, $form_state); + /* @var $formClass \Drupal\Core\Form\FormInterface */ + $formClass = $this->classResolver->getInstanceFromDefinition($operation['form']); + // Pass include any custom values for this operation. + if (!empty($operation['values'])) { + $cached_values = array_merge($cached_values, $operation['values']); + $form_state->setTemporaryValue('wizard', $cached_values); + } + // Build the form. + $form = $formClass->buildForm($form, $form_state); + if (isset($operation['title'])) { + $form['#title'] = $operation['title']; + } + $form['actions'] = $this->actions($formClass, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Only perform this logic if we're moving to the next page. This prevents + // the loss of cached values on ajax submissions. + if ((string)$form_state->getValue('op') == (string)$this->getNextOp()) { + $cached_values = $form_state->getTemporaryValue('wizard'); + if ($form_state->hasValue('label')) { + $cached_values['label'] = $form_state->getValue('label'); + } + if ($form_state->hasValue('id')) { + $cached_values['id'] = $form_state->getValue('id'); + } + if (is_null($this->machine_name) && !empty($cached_values['id'])) { + $this->machine_name = $cached_values['id']; + } + $this->getTempstore()->set($this->getMachineName(), $cached_values); + if (!$form_state->get('ajax')) { + $form_state->setRedirect($this->getRouteName(), $this->getNextParameters($cached_values)); + } + } + } + + /** + * {@inheritdoc} + */ + public function populateCachedValues(array &$form, FormStateInterface $form_state) { + $cached_values = $this->getTempstore()->get($this->getMachineName()); + if (!$cached_values) { + $cached_values = $form_state->getTemporaryValue('wizard'); + if (!$cached_values) { + $cached_values = $this->initValues(); + $form_state->setTemporaryValue('wizard', $cached_values); + } + } + } + + /** + * {@inheritdoc} + */ + public function previous(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $form_state->setRedirect($this->getRouteName(), $this->getPreviousParameters($cached_values)); + } + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + $this->getTempstore()->delete($this->getMachineName()); + } + + /** + * Helper function for generating default form elements. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return array + */ + protected function customizeForm(array $form, FormStateInterface $form_state) { + // Setup the step rendering theme element. + $prefix = [ + '#theme' => ['ctools_wizard_trail'], + '#wizard' => $this, + '#cached_values' => $form_state->getTemporaryValue('wizard'), + ]; + // @todo properly inject the renderer. + $form['#prefix'] = \Drupal::service('renderer')->render($prefix); + return $form; + } + + /** + * Generates action elements for navigating between the operation steps. + * + * @param \Drupal\Core\Form\FormInterface $form_object + * The current operation form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * + * @return array + */ + protected function actions(FormInterface $form_object, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $operations = $this->getOperations($cached_values); + $step = $this->getStep($cached_values); + $operation = $operations[$step]; + + $steps = array_keys($operations); + // Slice to find the operations that occur before the current operation. + $before = array_slice($operations, 0, array_search($step, $steps)); + // Slice to find the operations that occur after the current operation. + $after = array_slice($operations, array_search($step, $steps) + 1); + + $actions = [ + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Next'), + '#button_type' => 'primary', + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ], + ]; + + // Add any submit or validate functions for the step and the global ones. + if (isset($operation['validate'])) { + $actions['submit']['#validate'] = array_merge($actions['submit']['#validate'], $operation['validate']); + } + $actions['submit']['#validate'][] = '::validateForm'; + if (isset($operation['submit'])) { + $actions['submit']['#submit'] = array_merge($actions['submit']['#submit'], $operation['submit']); + } + $actions['submit']['#submit'][] = '::submitForm'; + + if ($form_state->get('ajax')) { + // Ajax submissions need to submit to the current step, not "next". + $parameters = $this->getNextParameters($cached_values); + $parameters['step'] = $this->getStep($cached_values); + $actions['submit']['#ajax'] = [ + 'callback' => '::ajaxSubmit', + 'url' => Url::fromRoute($this->getRouteName(), $parameters), + 'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], + ]; + } + + // If there are steps before this one, label the button "previous" + // otherwise do not display a button. + if ($before) { + $actions['previous'] = array( + '#type' => 'submit', + '#value' => $this->t('Previous'), + '#validate' => array( + array($this, 'populateCachedValues'), + ), + '#submit' => array( + array($this, 'previous'), + ), + '#limit_validation_errors' => array(), + '#weight' => -10, + ); + if ($form_state->get('ajax')) { + // Ajax submissions need to submit to the current step, not "previous". + $parameters = $this->getPreviousParameters($cached_values); + $parameters['step'] = $this->getStep($cached_values); + $actions['previous']['#ajax'] = [ + 'callback' => '::ajaxPrevious', + 'url' => Url::fromRoute($this->getRouteName(), $parameters), + 'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], + ]; + } + } + + // If there are not steps after this one, label the button "Finish". + if (!$after) { + $actions['submit']['#value'] = $this->t('Finish'); + $actions['submit']['#submit'][] = array($this, 'finish'); + if ($form_state->get('ajax')) { + $actions['submit']['#ajax']['callback'] = [$this, 'ajaxFinish']; + } + } + + return $actions; + } + + public function ajaxSubmit(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $response = new AjaxResponse(); + $parameters = $this->getNextParameters($cached_values); + $response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters)); + return $response; + } + + public function ajaxPrevious(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $response = new AjaxResponse(); + $parameters = $this->getPreviousParameters($cached_values); + $response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters)); + return $response; + } + + public function ajaxFinish(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand(new CloseModalDialogCommand()); + return $response; + } + + public function getRouteName() { + return $this->routeMatch->getRouteName(); + } + +} diff --git a/web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php b/web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php new file mode 100644 index 000000000..54043c3ba --- /dev/null +++ b/web/modules/contrib/ctools/src/Wizard/FormWizardInterface.php @@ -0,0 +1,182 @@ +getTempstore()->get($this->getMachineName()); + * + * @return string + */ + public function getStep($cached_values); + + /** + * Retrieve a list of FormInterface classes by their step key in the wizard. + * + * @param mixed $cached_values + * The values returned by $this->getTempstore()->get($this->getMachineName()); * + * + * @return array + * An associative array keyed on the step name with an array value with the + * following keys: + * - title (string): Human-readable title of the step. + * - form (string): Fully-qualified class name of the form for this step. + * - values (array): Optional array of cached values to override when on + * this step. + * - validate (array): Optional array of callables to be called when this + * step is validated. + * - submit (array): Optional array of callables to be called when this + * step is submitted. + */ + public function getOperations($cached_values); + + /** + * Retrieve the current Operation. + * + * @param mixed $cached_values + * The values returned by $this->getTempstore()->get($this->getMachineName()); + * + * @return string + * The class name to instantiate. + */ + public function getOperation($cached_values); + + /** + * The name of the route to which forward or backwards steps redirect. + * + * @return string + */ + public function getRouteName(); + + /** + * The Route parameters for a 'next' step. + * + * If your route requires more than machine_name and step keys, override and + * extend this method as needed. + * + * @param mixed $cached_values + * The values returned by $this->getTempstore()->get($this->getMachineName()); + * + * @return array + * An array keyed by: + * machine_name + * step + */ + public function getNextParameters($cached_values); + + /** + * The Route parameters for a 'previous' step. + * + * If your route requires more than machine_name and step keys, override and + * extend this method as needed. + * + * @param mixed $cached_values + * The values returned by $this->getTempstore()->get($this->getMachineName()); + * + * @return array + * An array keyed by: + * machine_name + * step + */ + public function getPreviousParameters($cached_values); + + /** + * Form validation handler that populates the cached values from tempstore. + * + * Temporary values are only available for a single page load so form + * submission will lose all the values. This was we reload and provide them + * to the validate and submit process. + * + * @param array $form + * Drupal form array + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The initial form state before validation or submission of the steps. + */ + public function populateCachedValues(array &$form, FormStateInterface $form_state); + + /** + * Form submit handler to step backwards in the wizard. + * + * "Next" steps are handled by \Drupal\Core\Form\FormInterface::submitForm(). + * + * @param array $form + * Drupal form array + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state of the wizard. This will not contain values from + * the current step since the previous button does not actually submit + * those values. + */ + public function previous(array &$form, FormStateInterface $form_state); + + /** + * Form submit handler for finalizing the wizard values. + * + * If you need to generate an entity or save config or raw table data + * subsequent to your form wizard, this is the responsible method. + * + * @param array $form + * Drupal form array + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The final form state of the wizard. + */ + public function finish(array &$form, FormStateInterface $form_state); + + public function ajaxSubmit(array $form, FormStateInterface $form_state); + + public function ajaxPrevious(array $form, FormStateInterface $form_state); + + public function ajaxFinish(array $form, FormStateInterface $form_state); + +} diff --git a/web/modules/contrib/ctools/src/Wizard/WizardFactory.php b/web/modules/contrib/ctools/src/Wizard/WizardFactory.php new file mode 100644 index 000000000..27522d066 --- /dev/null +++ b/web/modules/contrib/ctools/src/Wizard/WizardFactory.php @@ -0,0 +1,129 @@ +builder = $form_builder; + $this->dispatcher = $event_dispatcher; + } + + /** + * {@inheritdoc} + */ + public function getWizardForm(FormWizardInterface $wizard, array $parameters = [], $ajax = FALSE) { + $form_state = $this->getFormState($wizard, $parameters, $ajax); + $form = $this->builder->buildForm($wizard, $form_state); + + if ($ajax) { + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $status_messages = array('#type' => 'status_messages'); + // @todo properly inject the renderer. Core should really be doing this work. + if ($messages = \Drupal::service('renderer')->renderRoot($status_messages)) { + if (!empty($form['#prefix'])) { + // Form prefix is expected to be a string. Prepend the messages to + // that string. + $form['#prefix'] = '
' . $messages . '
' . $form['#prefix']; + } + } + } + return $form; + } + + /** + * @param string $class + * A class name implementing FormWizardInterface. + * @param array $parameters + * The array of parameters specific to this wizard. + * + * @return \Drupal\ctools\Wizard\FormWizardInterface + */ + public function createWizard($class, array $parameters) { + $arguments = []; + $reflection = new \ReflectionClass($class); + $constructor = $reflection->getMethod('__construct'); + foreach ($constructor->getParameters() as $parameter) { + if (array_key_exists($parameter->name, $parameters)) { + $arguments[] = $parameters[$parameter->name]; + } + elseif ($parameter->isDefaultValueAvailable()) { + $arguments[] = $parameter->getDefaultValue(); + } + } + /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface */ + $wizard = $reflection->newInstanceArgs($arguments); + return $wizard; + } + + /** + * Get the wizard form state. + * + * @param \Drupal\ctools\Wizard\FormWizardInterface $wizard + * The form wizard. + * @param array $parameters + * The array of parameters specific to this wizard. + * @param bool $ajax + * + * @return \Drupal\Core\Form\FormState + */ + public function getFormState(FormWizardInterface $wizard, array $parameters, $ajax = FALSE) { + $form_state = new FormState(); + // If a wizard has no values, initialize them. + if (!$wizard->getMachineName() || !$wizard->getTempstore()->get($wizard->getMachineName())) { + $cached_values = $wizard->initValues(); + // Save the cached values that were initialized. + if ($wizard->getMachineName()) { + $wizard->getTempstore()->set($wizard->getMachineName(), $cached_values); + } + } + else { + $cached_values = $wizard->getTempstore()->get($wizard->getMachineName()); + } + $form_state->setTemporaryValue('wizard', $cached_values); + $form_state->set('ajax', $ajax); + + $parameters['form'] = []; + $parameters['form_state'] = $form_state; + $method = new \ReflectionMethod($wizard, 'buildForm'); + $arguments = []; + foreach ($method->getParameters() as $parameter) { + if (array_key_exists($parameter->name, $parameters)) { + $arguments[] = $parameters[$parameter->name]; + } + elseif ($parameter->isDefaultValueAvailable()) { + $arguments[] = $parameter->getDefaultValue(); + } + } + unset($parameters['form'], $parameters['form_state']); + // Remove $form and $form_state from the arguments, and re-index them. + unset($arguments[0], $arguments[1]); + $form_state->addBuildInfo('args', array_values($arguments)); + return $form_state; + } + +} diff --git a/web/modules/contrib/ctools/src/Wizard/WizardFactoryInterface.php b/web/modules/contrib/ctools/src/Wizard/WizardFactoryInterface.php new file mode 100644 index 000000000..34b00a8f8 --- /dev/null +++ b/web/modules/contrib/ctools/src/Wizard/WizardFactoryInterface.php @@ -0,0 +1,42 @@ + + {% for key, value in trail %} + {% if key is same as(step) %} + {{ link(value.title, value.url) }} + {% else %} + {{ link(value.title, value.url) }} + {% endif %} + {% if value is not same as(trail|last) %} + {{ divider }} + {% endif %} + {% endfor %} + +{% endif %} diff --git a/web/modules/contrib/ctools/templates/ctools-wizard-trail.html.twig b/web/modules/contrib/ctools/templates/ctools-wizard-trail.html.twig new file mode 100644 index 000000000..308330789 --- /dev/null +++ b/web/modules/contrib/ctools/templates/ctools-wizard-trail.html.twig @@ -0,0 +1,14 @@ +{% if trail %} +
+ {% for key, value in trail %} + {% if key is same as(step) %} + {{ value }} + {% else %} + {{ value }} + {% endif %} + {% if value is not same as(trail|last) %} + {{ divider }} + {% endif %} + {% endfor %} +
+{% endif %} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/config/schema/ctools_wizard_test_config_entity.schema.yml b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/config/schema/ctools_wizard_test_config_entity.schema.yml new file mode 100644 index 000000000..f8e8c1463 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/config/schema/ctools_wizard_test_config_entity.schema.yml @@ -0,0 +1,18 @@ +ctools_wizard_test.ctools_wizard_test_config_entity.*: + type: config_entity + label: 'Example config entity' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + uuid: + type: string + one: + type: string + label: 'The first piece of information' + two: + type: string + label: 'The second piece of information' diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml new file mode 100644 index 000000000..76302ba10 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.info.yml @@ -0,0 +1,12 @@ +name: Chaos Wizard Test +type: module +description: 'Provides testing for ctools wizard' +package: Testing +# version: 3.x +# core: 8.x + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-3.0' +core: '8.x' +project: 'ctools' +datestamp: 1493401747 diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.action.yml b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.action.yml new file mode 100644 index 000000000..2b2c7e7f1 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.action.yml @@ -0,0 +1,6 @@ +entity.ctools_wizard_test_config_entity.add_form: + route_name: 'entity.ctools_wizard_test_config_entity.add_form' + title: 'Add Example config entity' + appears_on: + - entity.ctools_wizard_test_config_entity.collection + diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.menu.yml b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.menu.yml new file mode 100644 index 000000000..2d7bd0421 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.links.menu.yml @@ -0,0 +1,7 @@ +# Example config entity menu items definition +entity.ctools_wizard_test_config_entity.collection: + title: 'Example config entity' + route_name: entity.ctools_wizard_test_config_entity.collection + description: 'List Example config entity' + parent: system.admin_structure + diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.routing.yml b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.routing.yml new file mode 100644 index 000000000..141afb33e --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/ctools_wizard_test.routing.yml @@ -0,0 +1,82 @@ +ctools.wizard.test: + path: '/ctools/wizard' + defaults: + _wizard: '\Drupal\ctools_wizard_test\Wizard\WizardTest' + _title: 'Wizard Test' + tempstore_id: 'ctools.wizard.test' + machine_name: 'WizardTest' + requirements: + _access: 'TRUE' +ctools.wizard.test.step: + path: '/ctools/wizard/{step}' + defaults: + _wizard: '\Drupal\ctools_wizard_test\Wizard\WizardTest' + _title: 'Wizard Test' + tempstore_id: 'ctools.wizard.test' + machine_name: 'WizardTest' + requirements: + _access: 'TRUE' + +# ExampleConfigEntity routing definition +entity.ctools_wizard_test_config_entity.collection: + path: '/admin/structure/ctools_wizard_test_config_entity' + defaults: + _entity_list: 'ctools_wizard_test_config_entity' + _title: 'Example config entity' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE + +entity.ctools_wizard_test_config_entity.add_form: + path: '/admin/structure/ctools_wizard_test_config_entity/add' + defaults: + _entity_wizard: 'ctools_wizard_test_config_entity.add' + _title: 'Add Example config entity' + tempstore_id: 'ctools_wizard_test.config_entity' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE + +entity.ctools_wizard_test_config_entity.add_step_form: + path: '/admin/structure/ctools_wizard_test_config_entity/add/{machine_name}/{step}' + defaults: + _entity_wizard: 'ctools_wizard_test_config_entity.add' + _title: 'Add Example config entity' + tempstore_id: 'ctools_wizard_test.config_entity' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE + +entity.ctools_wizard_test_config_entity.edit_form: + path: '/admin/structure/ctools_wizard_test_config_entity/{machine_name}/{step}' + defaults: + _entity_wizard: 'ctools_wizard_test_config_entity.edit' + _title: 'Edit Example config entity' + tempstore_id: 'ctools_wizard_test.config_entity' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE + +entity.ctools_wizard_test_config_entity.external_form: + path: '/admin/structure/ctools_wizard_test_config_entity/{machine_name}/external' + defaults: + _title: 'Edit Example config entity' + _form: '\Drupal\ctools_wizard_test\Form\ExampleConfigEntityExternalForm' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE + +entity.ctools_wizard_test_config_entity.delete_form: + path: '/admin/structure/ctools_wizard_test_config_entity/{ctools_wizard_test_config_entity}/delete' + defaults: + _entity_form: 'ctools_wizard_test_config_entity.delete' + _title: 'Delete Example config entity' + requirements: + _permission: 'administer site configuration' + options: + _admin_route: TRUE diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Entity/ExampleConfigEntity.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Entity/ExampleConfigEntity.php new file mode 100644 index 000000000..b8dd1f50c --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Entity/ExampleConfigEntity.php @@ -0,0 +1,83 @@ +one; + } + + /** + * @inheritDoc + */ + public function getTwo() { + return $this->two; + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/ExampleConfigEntityInterface.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/ExampleConfigEntityInterface.php new file mode 100644 index 000000000..a4519e491 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/ExampleConfigEntityInterface.php @@ -0,0 +1,26 @@ +t('Example config entity'); + $header['id'] = $this->t('Machine name'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $this->getLabel($entity); + $row['id'] = $entity->id(); + // You probably want a few more properties here... + return $row + parent::buildRow($entity); + } + + /** + * @inheritDoc + */ + public function getOperations(EntityInterface $entity) { + $operations = parent::getOperations($entity); + + if (!empty($operations['edit'])) { + /** @var \Drupal\Core\Url $edit */ + $edit = $operations['edit']['url']; + $edit->setRouteParameters([ + 'machine_name' => $entity->id(), + 'step' => 'general', + ]); + } + + return $operations; + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityDeleteForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityDeleteForm.php new file mode 100644 index 000000000..487751d92 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityDeleteForm.php @@ -0,0 +1,52 @@ +t('Are you sure you want to delete %name?', array('%name' => $this->entity->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.ctools_wizard_test_config_entity.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + + drupal_set_message( + $this->t('content @type: deleted @label.', + [ + '@type' => $this->entity->bundle(), + '@label' => $this->entity->label() + ] + ) + ); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExistingForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExistingForm.php new file mode 100644 index 000000000..0ffca3dee --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExistingForm.php @@ -0,0 +1,37 @@ + '

This step only shows if the entity is already existing!

', + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Do nothing! + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php new file mode 100644 index 000000000..2ba871c28 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityExternalForm.php @@ -0,0 +1,66 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('user.shared_tempstore')); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ctools_wizard_test_example_config_entity_external_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $machine_name = '') { + $cached_values = $this->tempstore->get('ctools_wizard_test.config_entity')->get($machine_name); + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + $form['blah'] = [ + '#markup' => 'Value from one: ' . $config_entity->getOne(), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Don't do anything. + } + +} + diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php new file mode 100644 index 000000000..6120342e9 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityGeneralForm.php @@ -0,0 +1,46 @@ +getTemporaryValue('wizard'); + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + // The label and id will be added by the EntityFormWizardBase. + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + $config_entity->set('id', $form_state->getValue('id')); + $config_entity->set('label', $form_state->getValue('label')); + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php new file mode 100644 index 000000000..6d1cf6dcb --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityOneForm.php @@ -0,0 +1,62 @@ +getTemporaryValue('wizard'); + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + $form['one'] = [ + '#title' => $this->t('One'), + '#type' => 'textfield', + '#default_value' => $config_entity->getOne() ?: '', + ]; + + $form['external'] = [ + '#type' => 'link', + '#title' => $this->t('Show on dialog'), + '#url' => new Url('entity.ctools_wizard_test_config_entity.external_form', [ + 'machine_name' => $config_entity->id(), + ]), + '#attributes' => [ + 'class' => 'use-ajax', + 'data-dialog-type' => 'modal', + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + $config_entity->set('one', $form_state->getValue('one')); + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php new file mode 100644 index 000000000..6ea2f40bb --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/ExampleConfigEntityTwoForm.php @@ -0,0 +1,47 @@ +getTemporaryValue('wizard'); + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + $form['two'] = array( + '#title' => $this->t('Two'), + '#type' => 'textfield', + '#default_value' => $config_entity->getTwo() ?: '', + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + $config_entity->set('two', $form_state->getValue('two')); + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/OneForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/OneForm.php new file mode 100644 index 000000000..037505637 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/OneForm.php @@ -0,0 +1,71 @@ +getTemporaryValue('wizard'); + $form['one'] = [ + '#title' => $this->t('One'), + '#type' => 'textfield', + '#default_value' => !empty($cached_values['one']) ? $cached_values['one'] : '', + ]; + $form['dynamic'] = [ + '#title' => $this->t('Dynamic value'), + '#type' => 'item', + '#markup' => !empty($cached_values['dynamic']) ? $cached_values['dynamic'] : '', + ]; + return $form; + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $keys = array( + 'one', + ); + $cached_values = $form_state->getTemporaryValue('wizard'); + foreach ($keys as $key) { + $cached_values[$key] = $form_state->getValue($key); + } + $form_state->setTemporaryValue('wizard', $cached_values); + + drupal_set_message($this->t('Dynamic value submitted: @value', ['@value' => $cached_values['dynamic']]));; + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/TwoForm.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/TwoForm.php new file mode 100644 index 000000000..01ae10a78 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Form/TwoForm.php @@ -0,0 +1,69 @@ +getTemporaryValue('wizard'); + $form['two'] = [ + '#title' => $this->t('Two'), + '#type' => 'textfield', + '#default_value' => !empty($cached_values['two']) ? $cached_values['two'] : '', + ]; + $form['dynamic'] = [ + '#title' => $this->t('Dynamic value'), + '#type' => 'item', + '#markup' => !empty($cached_values['dynamic']) ? $cached_values['dynamic'] : '', + ]; + return $form; + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $keys = array( + 'two', + ); + $cached_values = $form_state->getTemporaryValue('wizard'); + foreach ($keys as $key) { + $cached_values[$key] = $form_state->getValue($key); + } + $form_state->setTemporaryValue('wizard', $cached_values); + } + +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php new file mode 100644 index 000000000..77ab5d599 --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/EntityAddWizardTest.php @@ -0,0 +1,15 @@ +t('Example entity'); + } + + /** + * {@inheritdoc} + */ + public function getMachineLabel() { + return $this->t('Label'); + } + + /** + * {@inheritdoc} + */ + public function getEntityType() { + return 'ctools_wizard_test_config_entity'; + } + + /** + * {@inheritdoc} + */ + public function exists() { + return '\Drupal\ctools_wizard_test\Entity\ExampleConfigEntity::load'; + } + + /** + * {@inheritdoc} + */ + public function getOperations($cached_values) { + /** @var $page \Drupal\ctools_wizard_test\Entity\ExampleConfigEntity */ + $config_entity = $cached_values['ctools_wizard_test_config_entity']; + + $steps = [ + 'general' => [ + 'form' => 'Drupal\ctools_wizard_test\Form\ExampleConfigEntityGeneralForm', + 'title' => $this->t('General'), + ], + 'one' => [ + 'form' => 'Drupal\ctools_wizard_test\Form\ExampleConfigEntityOneForm', + 'title' => $this->t('Form One'), + ], + 'two' => [ + 'form' => 'Drupal\ctools_wizard_test\Form\ExampleConfigEntityTwoForm', + 'title' => $this->t('Form Two'), + ], + ]; + + // To test that we can get the config entity and add/remove steps + // based on it's values, we'll add a special step only when the entity + // is pre-existing. + if (!empty($config_entity) && !$config_entity->isNew()) { + $steps['existing'] = [ + 'form' => 'Drupal\ctools_wizard_test\Form\ExampleConfigEntityExistingForm', + 'title' => $this->t('Existing entity'), + ]; + } + + return $steps; + } +} diff --git a/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php new file mode 100644 index 000000000..539df331d --- /dev/null +++ b/web/modules/contrib/ctools/tests/modules/ctools_wizard_test/src/Wizard/WizardTest.php @@ -0,0 +1,82 @@ +t('Wizard Information'); + } + + /** + * {@inheritdoc} + */ + public function getMachineLabel() { + return $this->t('Wizard Test Name'); + } + + /** + * {@inheritdoc} + */ + public function getOperations($cached_values) { + return array( + 'one' => [ + 'form' => 'Drupal\ctools_wizard_test\Form\OneForm', + 'title' => $this->t('Form One'), + 'values' => ['dynamic' => 'Xylophone'], + 'validate' => ['::stepOneValidate'], + 'submit' => ['::stepOneSubmit'], + ], + 'two' => [ + 'form' => 'Drupal\ctools_wizard_test\Form\TwoForm', + 'title' => $this->t('Form Two'), + 'values' => ['dynamic' => 'Zebra'], + ], + ); + } + + /** + * Validation callback for the first step. + */ + public function stepOneValidate($form, FormStateInterface $form_state) { + if ($form_state->getValue('one') == 'wrong') { + $form_state->setErrorByName('one', $this->t('Cannot set the value to "wrong".')); + } + } + + /** + * Submission callback for the first step. + */ + public function stepOneSubmit($form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + if ($form_state->getValue('one') == 'magic') { + $cached_values['one'] = 'Abraham'; + } + $form_state->setTemporaryValue('wizard', $cached_values); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return 'ctools.wizard.test.step'; + } + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + drupal_set_message($this->t('Value One: @one', ['@one' => $cached_values['one']])); + drupal_set_message($this->t('Value Two: @two', ['@two' => $cached_values['two']])); + parent::finish($form, $form_state); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php b/web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php new file mode 100644 index 000000000..94ca6d9c0 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Kernel/RelationshipManagerTest.php @@ -0,0 +1,63 @@ +relationshipManager->getDefinitions(); + $expected = [ + 'Bundle' => [ + 0 => "page", + 1 => "foo" + ] + ]; + $this->assertSame($expected, $definitions['typed_data_relationship:entity:node:body']['context']['base']->getConstraints()); + + // Check that typed data primitive labels are formatted properly. + $this->assertSame('Body from Page and Foo', (string) $definitions['typed_data_relationship:entity:node:body']['label']); + + // Check that entity relationship labels are formatted properly. + $this->assertSame('Authored by Entity from Content', (string) $definitions['typed_data_entity_relationship:entity:node:uid']['label']); + + // Check that language relationship labels are formatted properly. + $this->assertSame('Language Language from Content', (string) $definitions['typed_data_language_relationship:entity:node:langcode']['label']); + } + + /** + * @covers ::getDefinitionsForContexts + */ + public function testRelationshipPluginAvailability() { + $context_definition = new ContextDefinition('entity:node'); + $contexts = [ + 'node' => new Context($context_definition, $this->entities['node1']), + ]; + $definitions = $this->relationshipManager->getDefinitionsForContexts($contexts); + //$this->assertTrue(isset($definitions['typed_data_relationship:entity:node:body'])); + + $context_definition = new ContextDefinition('entity:node'); + $contexts = [ + 'node' => new Context($context_definition, $this->entities['node2']), + ]; + $definitions = $this->relationshipManager->getDefinitionsForContexts($contexts); + $this->assertFalse(isset($definitions['typed_data_relationship:entity:node:body'])); + + $context_definition = new ContextDefinition('entity:node'); + $contexts = [ + 'node' => new Context($context_definition, $this->entities['node3']), + ]; + $definitions = $this->relationshipManager->getDefinitionsForContexts($contexts); + //$this->assertTrue(isset($definitions['typed_data_relationship:entity:node:body'])); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php b/web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php new file mode 100644 index 000000000..cd58379bb --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Kernel/RelationshipsTestBase.php @@ -0,0 +1,96 @@ +installSchema('system', ['sequences', 'router']); + $this->installEntitySchema('user'); + $this->installEntitySchema('node_type'); + $this->installEntitySchema('node'); + $this->installConfig('node'); + $page = $this->createEntity('node_type', [ + 'type' => 'page', + 'name' => 'Page' + ]); + node_add_body_field($page); + $article = $this->createEntity('node_type', [ + 'type' => 'article', + 'name' => 'Article' + ]); + // Not adding the body field the articles so that we can perform a test. + $foo = $this->createEntity('node_type', [ + 'type' => 'foo', + 'name' => 'Foo' + ]); + node_add_body_field($foo); + $this->relationshipManager = $this->container->get('plugin.manager.ctools.relationship'); + + $user = $this->createEntity('user', [ + 'name' => 'test_user', + 'password' => 'password', + 'mail' => 'mail@test.com', + 'status' => 1, + ]); + $node1 = $this->createEntity('node', [ + 'title' => 'Node 1', + 'type' => 'page', + 'uid' => $user->id(), + 'body' => 'This is a test', + ]); + $node2 = $this->createEntity('node', [ + 'title' => 'Node 2', + 'type' => 'article', + 'uid' => $user->id() + ]); + $node3 = $this->createEntity('node', [ + 'title' => 'Node 3', + 'type' => 'foo', + 'uid' => $user->id() + ]); + + $this->entities = [ + 'user' => $user, + 'node1' => $node1, + 'node2' => $node2, + 'node3' => $node3, + ]; + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php b/web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php new file mode 100644 index 000000000..16e0704a9 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Kernel/SerializableTempstoreTest.php @@ -0,0 +1,64 @@ +installSchema('system', ['key_value_expire']); + } + + /** + * Tests serializing a serializable temp store object. + */ + public function testSerializableTempStore() { + $store = $this->container + ->get('ctools.serializable.tempstore.factory') + ->get('foobar'); + + // Add an unserializable request to the request stack. If the tempstore + // didn't use DependencySerializationTrait, the exception would be thrown + // when we try to serialize the tempstore. + $request = $this->prophesize(Request::class); + $request->willImplement('\Serializable'); + $request->serialize()->willThrow(new \LogicException('Not cool, bruh!')); + $this->container->get('request_stack')->push($request->reveal()); + + $this->assertInstanceOf(SerializableTempstore::class, $store); + /** @var SerializableTempstore $store */ + + $store = serialize($store); + $this->assertInternalType('string', $store); + $this->assertNotEmpty($store, 'The tempstore was serialized.'); + + $store = unserialize($store); + $this->assertInstanceOf(SerializableTempstore::class, $store, 'The tempstore was unserialized.'); + + $request_stack = $this->getObjectAttribute($store, 'requestStack'); + $this->assertSame( + $this->container->get('request_stack'), + $request_stack, + 'The request stack was pulled from the container during unserialization.' + ); + $this->assertSame($request->reveal(), $request_stack->pop()); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php new file mode 100644 index 000000000..042eb7b05 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataEntityRelationshipPluginTest.php @@ -0,0 +1,46 @@ +relationshipManager->createInstance('typed_data_entity_relationship:entity:node:type'); + $this->assertSame('type', $type_plugin->getName()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $uuid_plugin */ + $uid_plugin = $this->relationshipManager->createInstance('typed_data_entity_relationship:entity:node:uid'); + $this->assertSame('uid', $uid_plugin->getName()); + } + + /** + * @covers ::getRelationship + */ + public function testRelationship() { + /** @var \Drupal\ctools\Plugin\RelationshipInterface $type_plugin */ + $type_plugin = $this->relationshipManager->createInstance('typed_data_entity_relationship:entity:node:type'); + $type_plugin->setContextValue('base', $this->entities['node1']); + $relationship = $type_plugin->getRelationship(); + $this->assertTrue($relationship->getContextValue() instanceof NodeType); + $this->assertSame('entity:node_type', $relationship->getContextDefinition()->getDataType()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $uid_plugin */ + $uid_plugin = $this->relationshipManager->createInstance('typed_data_entity_relationship:entity:node:uid'); + $uid_plugin->setContextValue('base', $this->entities['node3']); + $relationship = $uid_plugin->getRelationship(); + $this->assertTrue($relationship->getContextValue() instanceof User); + $this->assertSame('entity:user', $relationship->getContextDefinition()->getDataType()); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Kernel/TypedDataLanguageRelationshipPluginTest.php b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataLanguageRelationshipPluginTest.php new file mode 100644 index 000000000..3e9453152 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataLanguageRelationshipPluginTest.php @@ -0,0 +1,36 @@ +relationshipManager->createInstance('typed_data_language_relationship:entity:node:langcode'); + $this->assertSame('langcode', $langcode_plugin->getName()); + } + + /** + * @covers ::getRelationship + * + * @todo expand to include a new language. + */ + public function testRelationship() { + /** @var \Drupal\ctools\Plugin\RelationshipInterface $langcode_plugin */ + $langcode_plugin = $this->relationshipManager->createInstance('typed_data_language_relationship:entity:node:langcode'); + $langcode_plugin->setContextValue('base', $this->entities['node1']); + $relationship = $langcode_plugin->getRelationship(); + $this->assertTrue($relationship->getContextValue() instanceof LanguageInterface); + $this->assertSame('en', $relationship->getContextValue()->getId()); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php new file mode 100644 index 000000000..f9a07cfb6 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataRelationshipPluginTest.php @@ -0,0 +1,103 @@ +relationshipManager->createInstance('typed_data_relationship:entity:node:nid'); + $this->assertSame('nid', $nid_plugin->getName()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $uuid_plugin */ + $uuid_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:uuid'); + $this->assertSame('uuid', $uuid_plugin->getName()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $title_plugin */ + $title_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:title'); + $this->assertSame('title', $title_plugin->getName()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $body_plugin */ + $body_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:body'); + $this->assertSame('body', $body_plugin->getName()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $uid_plugin */ + $uid_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:uid'); + $this->assertSame('uid', $uid_plugin->getName()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $mail_plugin */ + $mail_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:user:mail'); + $this->assertSame('mail', $mail_plugin->getName()); + } + + /** + * @covers ::getRelationship + */ + public function testRelationship() { + /** @var \Drupal\ctools\Plugin\RelationshipInterface $nid_plugin */ + $nid_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:nid'); + $nid_plugin->setContextValue('base', $this->entities['node1']); + $relationship = $nid_plugin->getRelationship(); + $this->assertTrue($relationship instanceof ContextInterface); + $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'integer'); + $this->assertTrue($relationship->hasContextValue()); + $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->id()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $uuid_plugin */ + $uuid_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:uuid'); + $uuid_plugin->setContextValue('base', $this->entities['node1']); + $relationship = $uuid_plugin->getRelationship(); + $this->assertTrue($relationship instanceof ContextInterface); + $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'string'); + $this->assertTrue($relationship->hasContextValue()); + $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->uuid()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $title_plugin */ + $title_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:title'); + $title_plugin->setContextValue('base', $this->entities['node1']); + $relationship = $title_plugin->getRelationship(); + $this->assertTrue($relationship instanceof ContextInterface); + $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'string'); + $this->assertTrue($relationship->hasContextValue()); + $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->label()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $body_plugin */ + $body_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:body'); + $body_plugin->setContextValue('base', $this->entities['node1']); + $relationship = $body_plugin->getRelationship(); + $this->assertTrue($relationship instanceof ContextInterface); + $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'string'); + $this->assertTrue($relationship->hasContextValue()); + $this->assertTrue($relationship->getContextValue() == $this->entities['node1']->get('body')->first()->get('value')->getValue()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $uid_plugin */ + $uid_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:node:uid'); + $uid_plugin->setContextValue('base', $this->entities['node3']); + $relationship = $uid_plugin->getRelationship(); + $this->assertTrue($relationship instanceof ContextInterface); + $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'integer'); + $this->assertTrue($relationship->hasContextValue()); + $this->assertTrue($relationship->getContextValue() == $this->entities['node3']->getOwnerId()); + + /** @var \Drupal\ctools\Plugin\RelationshipInterface $mail_plugin */ + $mail_plugin = $this->relationshipManager->createInstance('typed_data_relationship:entity:user:mail'); + $mail_plugin->setContextValue('base', $this->entities['user']); + $relationship = $mail_plugin->getRelationship(); + $this->assertTrue($relationship instanceof ContextInterface); + $this->assertTrue($relationship->getContextDefinition()->getDataType() == 'email'); + $this->assertTrue($relationship->hasContextValue()); + $this->assertTrue($relationship->getContextValue() == $this->entities['user']->getEmail()); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php new file mode 100644 index 000000000..c53c9c1f2 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Kernel/TypedDataResolverTest.php @@ -0,0 +1,105 @@ +installSchema('system', 'sequences'); + $this->installEntitySchema('user'); + + $this->typedDataResolver = \Drupal::service('ctools.typed_data.resolver'); + } + + /** + * Tests context extraction from properties. + */ + public function testGetContextFromProperty() { + // Create a user and test entity to extract context from. + $user = User::create(['uid' => 2, 'name' => 'username', 'mail' => 'mail@example.org']); + $user->enforceIsNew(TRUE); + $user->save(); + $entity_test = EntityTest::create(['user_id' => $user->id(), 'name' => 'Test name']); + + // Test the language property. + $property_context = $this->assertPropertyPath($entity_test, 'langcode:language', 'language'); + $this->assertEquals('en', $property_context->getContextValue()->getId()); + + // Test the reference to the user. + $property_context = $this->assertPropertyPath($entity_test, 'user_id:entity', 'entity:user'); + $this->assertEquals($user->id(), $property_context->getContextValue()->id()); + + // Test the reference to the name. + $property_context = $this->assertPropertyPath($entity_test, 'name:value', 'string'); + $this->assertEquals('Test name', $property_context->getContextValue()); + + // Test explicitly specifying the delta. + $property_context = $this->assertPropertyPath($entity_test, 'name:0:value', 'string'); + $this->assertEquals('Test name', $property_context->getContextValue()); + + // Test following the reference. + $property_context = $this->assertPropertyPath($entity_test, 'user_id:entity:mail:value', 'email'); + $this->assertEquals('mail@example.org', $property_context->getContextValue()); + } + + /** + * Asserts that a context for the given property path can be derived. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity to test with. + * @param $property_path + * The property path to look for. + * @param $expected_data_type + * The expected data type. + * + * @return \Drupal\Core\Plugin\Context\ContextInterface + * The context with a value. + */ + protected function assertPropertyPath(ContentEntityInterface $entity, $property_path, $expected_data_type) { + $typed_data_entity = $entity->getTypedData(); + $context_definition = new ContextDefinition($typed_data_entity->getDataDefinition()->getDataType()); + $context_with_value = new Context($context_definition, $typed_data_entity); + $context_without_value = new Context($context_definition); + + // Test the context without value. + $property_context = $this->typedDataResolver->getContextFromProperty($property_path, $context_without_value); + $this->assertEquals($expected_data_type, $property_context->getContextDefinition()->getDataType()); + + // Test the context with value. + $property_context = $this->typedDataResolver->getContextFromProperty($property_path, $context_with_value); + $this->assertEquals($expected_data_type, $property_context->getContextDefinition()->getDataType()); + + // Return the context with value so it can be asserted. + return $property_context; + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php b/web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php new file mode 100644 index 000000000..ed04f3d64 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Unit/BlockDisplayVariantTest.php @@ -0,0 +1,90 @@ +prophesize(AccountInterface::class); + $context_handler = $this->prophesize(ContextHandlerInterface::class); + $uuid_generator = $this->prophesize(UuidInterface::class); + $token = $this->prophesize(Token::class); + $block_manager = $this->prophesize(BlockManager::class); + $condition_manager = $this->prophesize(ConditionManager::class); + + $display_variant = new TestBlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal(), $block_manager->reveal(), $condition_manager->reveal()); + + $form = []; + $form_state = (new FormState())->setValues($values); + $display_variant->submitConfigurationForm($form, $form_state); + $this->assertSame($values['label'], $display_variant->label()); + } + + /** + * Provides data for testSubmitConfigurationForm(). + */ + public function providerTestSubmitConfigurationForm() { + $data = []; + $data[] = [ + [ + 'label' => 'test_label1', + ], + ]; + $data[] = [ + [ + 'label' => 'test_label2', + 'blocks' => ['foo1' => []], + ], + ]; + $data[] = [ + [ + 'label' => 'test_label3', + 'blocks' => ['foo1' => [], 'foo2' => []], + ], + ]; + return $data; + } + +} + +class TestBlockDisplayVariant extends BlockDisplayVariant { + + /** + * {@inheritdoc} + */ + public function build() { + return []; + } + + public function getRegionNames() { + return [ + 'top' => 'Top', + 'bottom' => 'Bottom', + ]; + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Unit/BlockPluginCollectionTest.php b/web/modules/contrib/ctools/tests/src/Unit/BlockPluginCollectionTest.php new file mode 100644 index 000000000..c9281070b --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Unit/BlockPluginCollectionTest.php @@ -0,0 +1,81 @@ + [ + 'id' => 'foo', + 'label' => 'Foo', + 'plugin' => 'system_powered_by_block', + 'region' => 'bottom', + ], + 'bar' => [ + 'id' => 'bar', + 'label' => 'Bar', + 'plugin' => 'system_powered_by_block', + 'region' => 'top', + ], + 'bing' => [ + 'id' => 'bing', + 'label' => 'Bing', + 'plugin' => 'system_powered_by_block', + 'region' => 'bottom', + 'weight' => -10, + ], + 'baz' => [ + 'id' => 'baz', + 'label' => 'Baz', + 'plugin' => 'system_powered_by_block', + 'region' => 'bottom', + ], + ]; + $block_manager = $this->prophesize(BlockManagerInterface::class); + $plugins = []; + foreach ($blocks as $block_id => $block) { + $plugin = $this->prophesize(BlockPluginInterface::class); + $plugin->label()->willReturn($block['label']); + $plugin->getConfiguration()->willReturn($block); + $plugins[$block_id] = $plugin->reveal(); + + $block_manager->createInstance($block_id, $block) + ->willReturn($plugin->reveal()) + ->shouldBeCalled(); + } + + + $block_plugin_collection = new BlockPluginCollection($block_manager->reveal(), $blocks); + $expected = [ + 'bottom' => [ + 'bing' => $plugins['bing'], + 'baz' => $plugins['baz'], + 'foo' => $plugins['foo'], + ], + 'top' => [ + 'bar' => $plugins['bar'], + ], + ]; + $this->assertSame($expected, $block_plugin_collection->getAllByRegion()); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php b/web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php new file mode 100644 index 000000000..892687943 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Unit/BlockVariantTraitTest.php @@ -0,0 +1,149 @@ +prophesize(BlockPluginCollection::class); + $block_collection->getAllByRegion() + ->willReturn($blocks) + ->shouldBeCalled(); + + $display_variant = new TestBlockVariantTrait(); + $display_variant->setBlockPluginCollection($block_collection->reveal()); + + $this->assertSame($expected, $display_variant->getRegionAssignments()); + } + + public function providerTestGetRegionAssignments() { + return [ + [ + [ + 'top' => [], + 'bottom' => [], + ], + ], + [ + [ + 'top' => ['foo'], + 'bottom' => [], + ], + [ + 'top' => ['foo'], + ], + ], + [ + [ + 'top' => [], + 'bottom' => [], + ], + [ + 'invalid' => ['foo'], + ], + ], + [ + [ + 'top' => [], + 'bottom' => ['foo'], + ], + [ + 'bottom' => ['foo'], + 'invalid' => ['bar'], + ], + ], + ]; + } + +} + +class TestBlockVariantTrait { + use BlockVariantTrait; + + /** + * @var array + */ + protected $blockConfig = []; + + /** + * @var \Drupal\Component\Uuid\UuidInterface + */ + protected $uuidGenerator; + + /** + * @param BlockPluginCollection $block_plugin_collection + * + * @return $this + */ + public function setBlockPluginCollection(BlockPluginCollection $block_plugin_collection) { + $this->blockPluginCollection = $block_plugin_collection; + return $this; + } + + /** + * @param \Drupal\Component\Uuid\UuidInterface $uuid_generator + * + * @return $this + */ + public function setUuidGenerator(UuidInterface $uuid_generator) { + $this->uuidGenerator = $uuid_generator; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function uuidGenerator() { + return $this->uuidGenerator; + } + + /** + * Sets the block configuration. + * + * @param array $config + * The block configuration. + * + * @return $this + */ + public function setBlockConfig(array $config) { + $this->blockConfig = $config; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function getBlockConfig() { + return $this->blockConfig; + } + + /** + * {@inheritdoc} + */ + public function getRegionNames() { + return [ + 'top' => 'Top', + 'bottom' => 'Bottom', + ]; + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Unit/ContextMapperTest.php b/web/modules/contrib/ctools/tests/src/Unit/ContextMapperTest.php new file mode 100644 index 000000000..24a85f286 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Unit/ContextMapperTest.php @@ -0,0 +1,104 @@ +typedDataManager = $this->prophesize(TypedDataManager::class); + $this->entityRepository = $this->prophesize(EntityRepositoryInterface::class); + $this->staticContext = new ContextMapper($this->entityRepository->reveal()); + + $container = new ContainerBuilder(); + $container->set('typed_data_manager', $this->typedDataManager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::getContextValues + */ + public function testGetContextValues() { + $input = []; + $actual = $this->staticContext->getContextValues($input); + $this->assertEquals([], $actual); + } + + /** + * @covers ::getContextValues + */ + public function testGetContextValuesContext() { + $data_definition = DataDefinition::createFromDataType('integer'); + $typed_data = IntegerData::createInstance($data_definition); + $this->typedDataManager->createDataDefinition('integer')->willReturn($data_definition); + $this->typedDataManager->getDefaultConstraints($data_definition)->willReturn([]); + $this->typedDataManager->create($data_definition, 5)->willReturn($typed_data); + + $input = [ + 'foo' => [ + 'label' => 'Foo', + 'description' => NULL, + 'type' => 'integer', + 'value' => 5, + ], + ]; + $expected = new Context(new ContextDefinition('integer', 'Foo'), 5); + $actual = $this->staticContext->getContextValues($input)['foo']; + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getContextValues + */ + public function testGetContextValuesEntityContext() { + $input = [ + 'foo' => [ + 'label' => 'Foo', + 'description' => NULL, + 'type' => 'entity:node', + 'value' => 'the_node_uuid', + ], + ]; + $expected = new EntityLazyLoadContext(new ContextDefinition('entity:node', 'Foo'), $this->entityRepository->reveal(), 'the_node_uuid'); + $actual = $this->staticContext->getContextValues($input)['foo']; + $this->assertEquals($expected, $actual); + } + +} diff --git a/web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php b/web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php new file mode 100644 index 000000000..4e93ea013 --- /dev/null +++ b/web/modules/contrib/ctools/tests/src/Unit/VariantCollectionTraitTest.php @@ -0,0 +1,209 @@ +manager = $this->prophesize(PluginManagerInterface::class); + $container->set('plugin.manager.display_variant', $this->manager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::getVariants + */ + public function testGetVariantsEmpty() { + $trait_object = new TestVariantCollectionTrait(); + $this->manager->createInstance()->shouldNotBeCalled(); + + $variants = $trait_object->getVariants(); + $this->assertInstanceOf(VariantPluginCollection::class, $variants); + $this->assertSame(0, count($variants)); + } + + /** + * @covers ::getVariants + */ + public function testGetVariants() { + $trait_object = new TestVariantCollectionTrait(); + $config = [ + 'foo' => ['id' => 'foo_plugin'], + 'bar' => ['id' => 'bar_plugin'], + ]; + foreach ($config as $value) { + $plugin = $this->prophesize(VariantInterface::class); + $this->manager->createInstance($value['id'], $value)->willReturn($plugin->reveal()); + } + $trait_object->setVariantConfig($config); + + $variants = $trait_object->getVariants(); + $this->assertInstanceOf(VariantPluginCollection::class, $variants); + $this->assertSame(2, count($variants)); + return $variants; + } + + /** + * @covers ::getVariants + * + * @depends testGetVariants + */ + public function testGetVariantsSort(VariantPluginCollection $variants) { + $this->assertSame(['bar' => 'bar', 'foo' => 'foo'], $variants->getInstanceIds()); + } + + /** + * @covers ::addVariant + */ + public function testAddVariant() { + $config = ['id' => 'foo']; + $uuid = 'test-uuid'; + $expected_config = $config + ['uuid' => $uuid]; + + $uuid_generator = $this->prophesize(UuidInterface::class); + $uuid_generator->generate() + ->willReturn($uuid) + ->shouldBeCalledTimes(1); + $trait_object = new TestVariantCollectionTrait(); + $trait_object->setUuidGenerator($uuid_generator->reveal()); + + $plugin_prophecy = $this->prophesize(VariantInterface::class); + $plugin_prophecy->getConfiguration() + ->willReturn($expected_config) + ->shouldBeCalled(); + $plugin_prophecy->setConfiguration($expected_config) + ->willReturn($expected_config) + ->shouldBeCalled(); + + $this->manager->createInstance('foo', $expected_config) + ->willReturn($plugin_prophecy->reveal()); + + $resulting_uuid = $trait_object->addVariant($config); + $this->assertSame($uuid, $resulting_uuid); + + $variants = $trait_object->getVariants(); + $this->assertSame([$uuid => $uuid], $variants->getInstanceIds()); + $this->assertSame([$uuid => $expected_config], $variants->getConfiguration()); + $this->assertSame($plugin_prophecy->reveal(), $variants->get($uuid)); + return [$trait_object, $uuid, $plugin_prophecy->reveal()]; + } + + /** + * @covers ::getVariant + * + * @depends testAddVariant + */ + public function testGetVariant($data) { + list($trait_object, $uuid, $plugin) = $data; + $this->manager->createInstance()->shouldNotBeCalled(); + + $this->assertSame($plugin, $trait_object->getVariant($uuid)); + return [$trait_object, $uuid]; + } + + /** + * @covers ::removeVariant + * + * @depends testGetVariant + */ + public function testRemoveVariant($data) { + list($trait_object, $uuid) = $data; + + $this->assertSame($trait_object, $trait_object->removeVariant($uuid)); + $this->assertFalse($trait_object->getVariants()->has($uuid)); + return [$trait_object, $uuid]; + } + + /** + * @covers ::getVariant + * + * @depends testRemoveVariant + * + * @expectedException \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @expectedExceptionMessage Plugin ID 'test-uuid' was not found. + */ + public function testGetVariantException($data) { + list($trait_object, $uuid) = $data; + // Attempt to retrieve a variant that has been removed. + $this->assertNull($trait_object->getVariant($uuid)); + } + +} + +class TestVariantCollectionTrait { + use VariantCollectionTrait; + + /** + * @var array + */ + protected $variantConfig = []; + + /** + * @var \Drupal\Component\Uuid\UuidInterface + */ + protected $uuidGenerator; + + /** + * @param \Drupal\Component\Uuid\UuidInterface $uuid_generator + * + * @return $this + */ + public function setUuidGenerator(UuidInterface $uuid_generator) { + $this->uuidGenerator = $uuid_generator; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function uuidGenerator() { + return $this->uuidGenerator; + } + + /** + * Sets the variant configuration. + * + * @param array $config + * The variant configuration. + * + * @return $this + */ + public function setVariantConfig(array $config) { + $this->variantConfig = $config; + return $this; + } + + /** + * {@inheritdoc} + */ + protected function getVariantConfig() { + return $this->variantConfig; + } + +} diff --git a/web/modules/contrib/pathauto/LICENSE.txt b/web/modules/contrib/pathauto/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/web/modules/contrib/pathauto/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/web/modules/contrib/pathauto/README.md b/web/modules/contrib/pathauto/README.md new file mode 100644 index 000000000..d21f1356a --- /dev/null +++ b/web/modules/contrib/pathauto/README.md @@ -0,0 +1,83 @@ +#Pathauto + +If you are developing for this module, have a look at pathauto.api.php. + +##Description + +The Pathauto module provides support functions for other modules to +automatically generate aliases based on appropriate criteria, with a +central settings path for site administrators. + +Implementations are provided for core entity types: content, taxonomy terms, +and users (including blogs and forum pages). + +Pathauto also provides a way to delete large numbers of aliases. This feature +is available at Administer > Configuration > Search and metadata > URL aliases > +Delete aliases. + +##Benefits + +Besides making the page address more reflective of its content than +"node/138", it's important to know that modern search engines give +heavy weight to search terms which appear in a page's URL. By +automatically using keywords based directly on the page content in the URL, +relevant search engine hits for your page can be significantly +enhanced. + +##Notices + +Pathauto just adds URL aliases to content, users, and taxonomy terms. +Because it's an alias, the standard Drupal URL (for example node/123 or +taxonomy/term/1) will still function as normal. If you have external links +to your site pointing to standard Drupal URLs, or hardcoded links in a module, +template, content or menu which point to standard Drupal URLs it will bypass +the alias set by Pathauto. + +There are reasons you might not want two URLs for the same content on your +site. If this applies to you, please note that you will need to update any +hard coded links in your content or blocks. + +If you use the "system path" (i.e. node/10) for menu items and settings like +that, Drupal will replace it with the url_alias. + +For external links, you might want to consider the Path Redirect or +Global Redirect modules, which allow you to set forwarding either per item or +across the site to your aliased URLs. + +###URLs (not) Getting Replaced With Aliases: +Please bear in mind that only URLs passed through Drupal's Drupal's URL and +Link APIs will be replaced with their aliases during page output. If +a module or your template contains hardcoded links, such as +'href="node/$node->nid"', those won't get replaced with their corresponding +aliases. + +## Disabling Pathauto for a specific content type (or taxonomy) + +When the pattern for a content type is left blank, the default pattern will be +used. But if the default pattern is also blank, Pathauto will be disabled +for that content type. + +## Installing Pathauto +1. Install the module as normal, note that there are two dependencies. +2. Configure the module at admin/config/search/path/patterns - add a new pattern by creating by clicking "Add Pathauto pattern". +3. Fill out "Path pattern" with fx [node:title], choose which content types this applies to, give it a label (the name) and save it. +4. When you save new content from now on, it should automatically be assigned an alternative URL + +##Credits: + +The original module combined the functionality of Mike Ryan's autopath with +Tommy Sundstrom's path_automatic. + +Significant enhancements were contributed by jdmquin @ www.bcdems.net. + +Matt England added the tracker support (tracker support has been removed in +recent changes). + +Other suggestions and patches contributed by the Drupal community. + +Current maintainers: + +- Dave Reid - http://www.davereid.net +- Greg Knaddison - http://www.knaddison.com +- Mike Ryan - http://mikeryan.name +- Frederik 'Freso' S. Olesen - http://freso.dk diff --git a/web/modules/contrib/pathauto/config/install/pathauto.settings.yml b/web/modules/contrib/pathauto/config/install/pathauto.settings.yml new file mode 100644 index 000000000..74a75cdd2 --- /dev/null +++ b/web/modules/contrib/pathauto/config/install/pathauto.settings.yml @@ -0,0 +1,13 @@ +enabled_entity_types: + - user +punctuation: + hyphen: 1 +verbose : FALSE +separator : '-' +max_length : 100 +max_component_length: 100 +transliterate : TRUE +reduce_ascii : FALSE +case : TRUE +ignore_words : 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with' +update_action : 2 diff --git a/web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_node.yml b/web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_node.yml new file mode 100644 index 000000000..07709b7c0 --- /dev/null +++ b/web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_node.yml @@ -0,0 +1,9 @@ +id: pathauto_update_alias_node +label: 'Update URL alias' +status: true +langcode: en +type: node +plugin: pathauto_update_alias +dependencies: + module: + - node diff --git a/web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_user.yml b/web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_user.yml new file mode 100644 index 000000000..891cbf220 --- /dev/null +++ b/web/modules/contrib/pathauto/config/optional/system.action.pathauto_update_alias_user.yml @@ -0,0 +1,9 @@ +id: pathauto_update_alias_user +label: 'Update URL alias' +status: true +langcode: en +type: user +plugin: pathauto_update_alias +dependencies: + module: + - user diff --git a/web/modules/contrib/pathauto/config/schema/pathauto.schema.yml b/web/modules/contrib/pathauto/config/schema/pathauto.schema.yml new file mode 100644 index 000000000..ca04411e8 --- /dev/null +++ b/web/modules/contrib/pathauto/config/schema/pathauto.schema.yml @@ -0,0 +1,36 @@ +pathauto.settings: + type: config_object + mapping: + enabled_entity_types: + label: Enabled entity types + type: sequence + sequence: + type: string + punctuation: + type: sequence + sequence: + type: integer + verbose: + type: boolean + separator: + type: string + max_length: + type: integer + max_component_length: + type: integer + transliterate: + type: boolean + reduce_ascii: + type: boolean + ignore_words: + type: string + case: + type: boolean + ignore_words: + type: string + update_action: + type: integer + +action.configuration.pathauto_update_alias: + type: action_configuration_default + label: 'Update URL alias' diff --git a/web/modules/contrib/pathauto/config/schema/pathauto_pattern.schema.yml b/web/modules/contrib/pathauto/config/schema/pathauto_pattern.schema.yml new file mode 100644 index 000000000..14a1ae7b6 --- /dev/null +++ b/web/modules/contrib/pathauto/config/schema/pathauto_pattern.schema.yml @@ -0,0 +1,40 @@ +pathauto.pattern.*: + type: config_entity + label: 'Pathauto pattern config' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + uuid: + type: string + type: + type: string + label: 'Pattern type' + pattern: + type: string + label: 'Pattern' + selection_criteria: + type: sequence + label: 'Selection criteria' + sequence: + type: condition.plugin.[id] + label: 'Selection condition' + selection_logic: + type: string + label: 'Selection logic' + weight: + type: integer + label: 'Weight' + relationships: + type: sequence + label: 'Context definitions' + sequence: + - type: mapping + label: 'Relationship' + mapping: + label: + type: label + label: 'Label' diff --git a/web/modules/contrib/pathauto/pathauto.api.php b/web/modules/contrib/pathauto/pathauto.api.php new file mode 100644 index 000000000..fc9272f8f --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.api.php @@ -0,0 +1,160 @@ +query("SELECT 1 FROM {mytable} WHERE path = :path", [':path' => $alias])->fetchField(); +} + +/** + * Alter the pattern to be used before an alias is generated by Pathauto. + * + * This hook will only be called if a default pattern is configured (on + * admin/config/search/path/patterns). + * + * @param string $pattern + * The alias pattern for Pathauto to pass to token_replace() to generate the + * URL alias. + * @param array $context + * An associative array of additional options, with the following elements: + * - 'module': The module or entity type being aliased. + * - 'op': A string with the operation being performed on the object being + * aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'. + * - 'source': A string of the source path for the alias (e.g. 'node/1'). + * - 'data': An array of keyed objects to pass to token_replace(). + * - 'type': The sub-type or bundle of the object being aliased. + * - 'language': A string of the language code for the alias (e.g. 'en'). + * This can be altered by reference. + */ +function hook_pathauto_pattern_alter(&$pattern, array $context) { + // Switch out any [node:created:*] tokens with [node:updated:*] on update. + if ($context['module'] == 'node' && ($context['op'] == 'update')) { + $pattern = preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern); + } +} + +/** + * Alter Pathauto-generated aliases before saving. + * + * @param string $alias + * The automatic alias after token replacement and strings cleaned. + * @param array $context + * An associative array of additional options, with the following elements: + * - 'module': The module or entity type being aliased. + * - 'op': A string with the operation being performed on the object being + * aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'. + * - 'source': A string of the source path for the alias (e.g. 'node/1'). + * This can be altered by reference. + * - 'data': An array of keyed objects to pass to token_replace(). + * - 'type': The sub-type or bundle of the object being aliased. + * - 'language': A string of the language code for the alias (e.g. 'en'). + * This can be altered by reference. + * - 'pattern': A string of the pattern used for aliasing the object. + */ +function hook_pathauto_alias_alter(&$alias, array &$context) { + // Add a suffix so that all aliases get saved as 'content/my-title.html' + $alias .= '.html'; + + // Force all aliases to be saved as language neutral. + $context['language'] = Language::LANGCODE_NOT_SPECIFIED; +} + +/** + * Alter the list of punctuation characters for Pathauto control. + * + * @param $punctuation + * An array of punctuation to be controlled by Pathauto during replacement + * keyed by punctuation name. Each punctuation record should be an array + * with the following key/value pairs: + * - value: The raw value of the punctuation mark. + * - name: The human-readable name of the punctuation mark. This must be + * translated using t() already. + */ +function hook_pathauto_punctuation_chars_alter(array &$punctuation) { + // Add the trademark symbol. + $punctuation['trademark'] = array('value' => '™', 'name' => t('Trademark symbol')); + + // Remove the dollar sign. + unset($punctuation['dollar']); +} diff --git a/web/modules/contrib/pathauto/pathauto.info.yml b/web/modules/contrib/pathauto/pathauto.info.yml new file mode 100644 index 000000000..d34ea43aa --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.info.yml @@ -0,0 +1,20 @@ +name : 'Pathauto' +description : 'Provides a mechanism for modules to automatically generate aliases for the content they manage.' +# core: 8.x +type: module + +dependencies: +- ctools:ctools +- drupal:path +- token:token + +configure: entity.pathauto_pattern.collection + +recommends: +- redirect:redirect + +# Information added by Drupal.org packaging script on 2017-04-29 +version: '8.x-1.0' +core: '8.x' +project: 'pathauto' +datestamp: 1493468049 diff --git a/web/modules/contrib/pathauto/pathauto.install b/web/modules/contrib/pathauto/pathauto.install new file mode 100644 index 000000000..f8de322f6 --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.install @@ -0,0 +1,301 @@ +schema(); + if (!$database_schema->tableExists($alias_storage::TABLE)) { + $schema_definition = $alias_storage->schemaDefinition(); + $database_schema->createTable($alias_storage::TABLE, $schema_definition); + } + } +} + +/** + * Updates pathauto widgets to use the path widget ID. + */ +function pathauto_update_8001() { + + // Replace values in the 'entity.definitions.installed' keyvalue collection. + $collection = \Drupal::service('keyvalue')->get('entity.definitions.installed'); + foreach ($collection->getAll() as $key => $definitions) { + if (!is_array($definitions) || empty($definitions['path'])) { + continue; + } + + // Retrieve and change path base field definition. + $path_definition = $definitions['path']; + if (($options = $path_definition->getDisplayOptions('form')) && $options['type'] = 'pathauto') { + $options['type'] = 'path'; + $path_definition->setDisplayOptions('form', $options); + // Save the new value. + $collection->set($key, $definitions); + } + + } + + foreach (EntityFormDisplay::loadMultiple() as $form_display) { + if ($component = $form_display->getComponent('path')) { + if (isset($component['type']) && $component['type'] == 'pathauto') { + $component['type'] = 'path'; + $form_display->setComponent('path', $component); + $form_display->save(); + } + } + } +} + +/** + * Converts patterns from configuration objects to configuration entities. + */ +function pathauto_update_8100() { + \Drupal::service('module_installer')->install(['ctools']); + + $messages = array(); + /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info */ + $entity_bundle_info = \Drupal::service('entity_type.bundle.info'); + $entity_type_manager = \Drupal::entityTypeManager(); + $language_manager = \Drupal::languageManager(); + $entity_type_manager->clearCachedDefinitions(); + \Drupal::service('plugin.manager.alias_type')->clearCachedDefinitions(); + $entity_types = $entity_type_manager->getDefinitions(); + + // 1. Load all patterns. + $config = \Drupal::configFactory()->getEditable('pathauto.pattern'); + $patterns = $config->get('patterns'); + + // 2. Create a configuration entity per pattern. + foreach ($patterns as $entity_type => $entity_patterns) { + if (!array_key_exists($entity_type, $entity_types)) { + // We found an unknown entity type. Report it. + $messages[] = t('Entity of type @type was not processed. It defines the following patterns: @patterns', array( + '@type' => $entity_type, + '@patterns' => print_r($entity_patterns, TRUE), + )); + continue; + } + $entity_label = $entity_types[$entity_type]->getLabel(); + + if (!empty($entity_patterns['default'])) { + // This is a pattern for an entity type, such as "node". + $pattern = PathautoPattern::create([ + 'id' => $entity_type, + 'label' => $entity_label, + 'type' => 'canonical_entities:' . $entity_type, + 'pattern' => $entity_patterns['default'], + 'weight' => 0, + ]); + $pattern->save(); + } + + // Loop over bundles and create patterns if they have a value. + // Bundle keys may have a language suffix for language-dependant patterns. + if (isset($entity_patterns['bundles'])) { + $bundle_info = $entity_bundle_info->getBundleInfo($entity_type); + foreach ($entity_patterns['bundles'] as $bundle => $bundle_patterns) { + if (empty($bundle_patterns['default'])) { + // This bundle does not define a pattern. Move on to the next one. + continue; + } + + if (isset($bundle_info[$bundle])) { + // This is a pattern for a bundle, such as "node_article". + $pattern = PathautoPattern::create([ + 'id' => $entity_type . '_' . $bundle, + 'label' => $entity_label . ' ' . $bundle_info[$bundle]['label'], + 'type' => 'canonical_entities:' . $entity_type, + 'pattern' => $bundle_patterns['default'], + 'weight' => -5, + ]); + + // Add the bundle condition. + $pattern->addSelectionCondition([ + 'id' => 'entity_bundle:' . $entity_type, + 'bundles' => array($bundle => $bundle), + 'negate' => FALSE, + 'context_mapping' => [ $entity_type => $entity_type ], + ]); + + $pattern->save(); + } + else { + // This is either a language dependent pattern such as "article_es" or + // an unknown bundle or langcode. Let's figure it out. + $matches = NULL; + $langcode = NULL; + $extracted_bundle = NULL; + $language = NULL; + preg_match('/^(.*)_([a-z-]*)$/', $bundle, $matches); + if (count($matches) == 3) { + list(, $extracted_bundle, $langcode) = $matches; + $language = $language_manager->getLanguage($langcode); + } + // Validate bundle, langcode and language. + if (!isset($bundle_info[$extracted_bundle]) || ($langcode == NULL) || ($language == NULL)) { + $messages[] = t('Unrecognized entity bundle @entity:@bundle was not processed. It defines the following patterns: @patterns', array( + '@entity' => $entity_type, + '@bundle' => $bundle, + '@patterns' => print_r($entity_patterns, TRUE), + )); + continue; + } + + // This is a pattern for a bundle and a language, such as "node_article_es". + $pattern = PathautoPattern::create([ + 'id' => $entity_type . '_' . $extracted_bundle . '_' . str_replace('-', '_', $langcode), + 'label' => $entity_label . ' ' . $bundle_info[$extracted_bundle]['label'] . ' ' . $language->getName(), + 'type' => 'canonical_entities:' . $entity_type, + 'pattern' => $bundle_patterns['default'], + 'weight' => -10, + ]); + + // Add the bundle condition. + $pattern->addSelectionCondition([ + 'id' => 'entity_bundle:' . $entity_type, + 'bundles' => array($extracted_bundle => $extracted_bundle), + 'negate' => FALSE, + 'context_mapping' => [ $entity_type => $entity_type ], + ]); + + // Add the language condition. + $language_mapping = $entity_type . ':' . $entity_type_manager->getDefinition($entity_type)->getKey('langcode') . ':language'; + $pattern->addSelectionCondition([ + 'id' => 'language', + 'langcodes' => [ $langcode => $langcode ], + 'negate' => FALSE, + 'context_mapping' => [ + 'language' => $language_mapping, + ] + ]); + + // Add the context relationship for this language. + $pattern->addRelationship($language_mapping, 'Language'); + + $pattern->save(); + } + } + } + } + + // 3. Delete the old configuration object that stores patterns. + $config->delete(); + + // 4. Print out messages. + if (!empty($messages)) { + return implode('
', $messages); + } +} + +/** + * Update relationship storage. + */ +function pathauto_update_8101() { + foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) { + $pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name); + + $relationships = []; + foreach ((array) $pattern_config->get('context_definitions') as $context_definition) { + $relationships[$context_definition['id']] = ['label' => $context_definition['label']]; + } + + $pattern_config->clear('context_definitions'); + $pattern_config->set('relationships', $relationships); + $pattern_config->save(); + } +} + +/** + * Update node type conditions from entity_bundle to node_type. + */ +function pathauto_update_8102() { + // Load all pattern configuration entities. + foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) { + $pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name); + + // Loop patterns and swap the entity_bundle:node plugin by the node_type + // plugin. + if ($pattern_config->get('type') == 'canonical_entities:node') { + $selection_criteria = $pattern_config->get('selection_criteria'); + foreach ($selection_criteria as $uuid => $condition) { + if ($condition['id'] == 'entity_bundle:node') { + $selection_criteria[$uuid]['id'] = 'node_type'; + $pattern_config->set('selection_criteria', $selection_criteria); + $pattern_config->save(); + break; + } + } + } + } +} + +/** + * Fix invalid default value for ignore_words. + */ +function pathauto_update_8103() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('pathauto.settings'); + $ignore_words = $config->get('ignore_words'); + if ($ignore_words === ', in, is,that, the , this, with, ') { + $config->set('ignore_words', 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with')->save(TRUE); + } +} + +/** + * Resave patterns so that lookup keys are updated. + */ +function pathauto_update_8104() { + \Drupal::entityTypeManager()->clearCachedDefinitions(); + // Load all pattern configuration entities and save them, so that the new + // status lookup keys are saved. + foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) { + $pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name); + $pattern_config->save(); + } +} + +/** + * Ensure the url_alias table exists. + */ +function pathauto_update_8105() { + _pathauto_ensure_url_alias_table_exists(); +} + +/** + * Update default configuration for enabled entity types. + */ +function pathauto_update_8106() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('pathauto.settings'); + $config->set('enabled_entity_types', ['user']); + $config->save(); +} diff --git a/web/modules/contrib/pathauto/pathauto.js b/web/modules/contrib/pathauto/pathauto.js new file mode 100644 index 000000000..d4e068f6b --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.js @@ -0,0 +1,21 @@ +(function ($) { + 'use strict'; + Drupal.behaviors.pathFieldsetSummaries = { + attach: function (context) { + $('fieldset.path-form', context).drupalSetSummary(function (context) { + var path = $('.form-item-path-alias input', context).val(); + var automatic = $('.form-item-path-pathauto input', context).attr('checked'); + + if (automatic) { + return Drupal.t('Automatic alias'); + } + else if (path) { + return Drupal.t('Alias: @alias', {'@alias': path}); + } + else { + return Drupal.t('No alias'); + } + }); + } + }; +})(jQuery); diff --git a/web/modules/contrib/pathauto/pathauto.libraries.yml b/web/modules/contrib/pathauto/pathauto.libraries.yml new file mode 100644 index 000000000..ae978aa42 --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.libraries.yml @@ -0,0 +1,9 @@ +widget: + version: 1.0 + js: + pathauto.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings + - core/drupal.form diff --git a/web/modules/contrib/pathauto/pathauto.links.action.yml b/web/modules/contrib/pathauto/pathauto.links.action.yml new file mode 100644 index 000000000..d68b94a88 --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.links.action.yml @@ -0,0 +1,6 @@ +entity.pathauto_pattern.add_form: + route_name: 'entity.pathauto_pattern.add_form' + title: 'Add Pathauto pattern' + appears_on: + - entity.pathauto_pattern.collection + diff --git a/web/modules/contrib/pathauto/pathauto.links.task.yml b/web/modules/contrib/pathauto/pathauto.links.task.yml new file mode 100644 index 000000000..d6fa0bca5 --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.links.task.yml @@ -0,0 +1,23 @@ +pathauto.patterns.form: + route_name: entity.pathauto_pattern.collection + base_route: path.admin_overview + title: 'Patterns' + weight: 10 + +pathauto.settings.form: + route_name: pathauto.settings.form + base_route: path.admin_overview + title: 'Settings' + weight: 20 + +pathauto.bulk.update.form: + route_name: pathauto.bulk.update.form + base_route: path.admin_overview + title: 'Bulk generate' + weight: 30 + +pathauto.admin.delete: + route_name: pathauto.admin.delete + base_route: path.admin_overview + title: 'Delete aliases' + weight: 40 diff --git a/web/modules/contrib/pathauto/pathauto.module b/web/modules/contrib/pathauto/pathauto.module new file mode 100644 index 000000000..d0763fa17 --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.module @@ -0,0 +1,188 @@ + 'pathauto')); +} + +/** + * Implements hook_help(). + */ +function pathauto_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.pathauto': + $output = '

' . t('About') . '

'; + $output .= '

' . t('The Pathauto module provides a mechanism to automate the creation of path aliases. This makes URLs more readable and helps search engines index content more effectively. For more information, see the online documentation for Pathauto.', [':online' => 'https://www.drupal.org/documentation/modules/pathauto']) . '

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

' . t('Uses') . '

'; + $output .= '
' . t('Pathauto is accessed from the tabs it adds to the list of URL aliases.', [':aliases' => Url::fromRoute('path.admin_overview')->toString()]) . '
'; + $output .= '
' . t('Creating Pathauto Patterns') . '
'; + $output .= '
' . t('The "Patterns" page is used to configure automatic path aliasing. New patterns are created here using the Add Pathauto pattern button which presents a form to simplify pattern creation thru the use of available tokens. The patterns page provides a list of all patterns on the site and allows you to edit and reorder them. An alias is generated for the first pattern that applies.', [':pathauto_pattern' => Url::fromRoute('entity.pathauto_pattern.collection')->toString(), ':add_form' => Url::fromRoute('entity.pathauto_pattern.add_form')->toString()]) . '
'; + $output .= '
' . t('Pathauto Settings') . '
'; + $output .= '
' . t('The "Settings" page is used to customize global Pathauto settings for automated pattern creation.', [':settings' => Url::fromRoute('pathauto.settings.form')->toString()]) . '
'; + $output .= '
' . t('The maximum alias length and maximum component length values default to 100 and have a limit of @max from Pathauto. You should enter a value that is the length of the "alias" column of the url_alias database table minus the length of any strings that might get added to the end of the URL. The recommended and default value is 100.', array('@max' => \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength())) . '
'; + $output .= '
' . t('Bulk Generation') . '
'; + $output .= '
' . t('The "Bulk Generate" page allows you to create URL aliases for items that currently have no aliases. This is typically used when installing Pathauto on a site that has existing un-aliased content that needs to be aliased in bulk.', [':pathauto_bulk' => Url::fromRoute('pathauto.bulk.update.form')->toString()]) . '
'; + $output .= '
' . t('Delete Aliases') . '
'; + $output .= '
' . t('The "Delete Aliases" page allows you to remove URL aliases from items that have previously been assigned aliases using pathauto.', [':pathauto_delete' => Url::fromRoute('pathauto.admin.delete')->toString()]) . '
'; + $output .= '
'; + return $output; + + case 'entity.pathauto_pattern.collection': + $output = '

' . t('This page provides a list of all patterns on the site and allows you to edit and reorder them.') . '

'; + return $output; + + case 'entity.pathauto_pattern.add_form': + $output = '

' . t('You need to select a pattern type, then a pattern and filter, and a label. Additional types can be enabled on the Settings page.', [':settings' => Url::fromRoute('pathauto.settings.form')->toString()]) . '

'; + return $output; + + case 'pathauto.bulk.update.form': + $output = '

' . t('Bulk generation can be used to generate URL aliases for items that currently have no aliases. This is typically used when installing Pathauto on a site that has existing un-aliased content that needs to be aliased in bulk.') . '
'; + $output .= t('It can also be used to regenerate URL aliases for items that have an old alias and for which the Pathauto pattern has been changed.') . '

'; + $output .= '

' . t('Note that this will only affect items which are configured to have their URL alias automatically set. Items whose URL alias is manually set are not affected.') . '

'; + return $output; + } +} + +/** + * Implements hook_entity_insert(). + */ +function pathauto_entity_insert(EntityInterface $entity) { + \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'insert'); +} + +/** + * Implements hook_entity_update(). + */ +function pathauto_entity_update(EntityInterface $entity) { + \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'update'); +} + +/** + * Implements hook_entity_update(). + */ +function pathauto_entity_delete(EntityInterface $entity) { + if ($entity->hasLinkTemplate('canonical') && $entity instanceof ContentEntityInterface && $entity->hasField('path')) { + \Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($entity); + $entity->path->first()->get('pathauto')->purge(); + } +} + +/** + * Implements hook_field_info_alter(). + */ +function pathauto_field_info_alter(&$info) { + $info['path']['class'] = '\Drupal\pathauto\PathautoItem'; +} + +/** + * Implements hook_field_widget_info_alter(). + */ +function pathauto_field_widget_info_alter(&$widgets) { + $widgets['path']['class'] = 'Drupal\pathauto\PathautoWidget'; +} + +/** + * Implements hook_entity_base_field_info(). + */ +function pathauto_entity_base_field_info(EntityTypeInterface $entity_type) { + $config = \Drupal::config('pathauto.settings'); + // Verify that the configuration data isn't null (as is the case before the + // module's initialization, in tests), so that in_array() won't fail. + if ($enabled_entity_types = $config->get('enabled_entity_types')) { + if (in_array($entity_type->id(), $enabled_entity_types)) { + $fields['path'] = BaseFieldDefinition::create('path') + ->setCustomStorage(TRUE) + ->setLabel(t('URL alias')) + ->setTranslatable(TRUE) + ->setComputed(TRUE) + ->setDisplayOptions('form', array( + 'type' => 'path', + 'weight' => 30, + )) + ->setDisplayConfigurable('form', TRUE); + + return $fields; + } + } +} + +/** + * Implements hook_entity_base_field_info_alter(). + */ +function pathauto_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { + if (isset($fields['path'])) { + // Path fields need to be computed so that the pathauto state can be + // accessed even if there is no alias being set. + $fields['path']->setComputed(TRUE); + } +} + +/** + * Validate the pattern field, to ensure it doesn't contain any characters that + * are invalid in URLs. + */ +function pathauto_pattern_validate($element, FormStateInterface $form_state) { + + if (isset($element['#value'])) { + $title = empty($element['#title']) ? $element['#parents'][0] : $element['#title']; + $invalid_characters = ['#', '?', '&']; + $invalid_characters_used = []; + + foreach ($invalid_characters as $invalid_character) { + if (strpos($element['#value'], $invalid_character) !== FALSE) { + $invalid_characters_used[] = $invalid_character; + } + } + + if (!empty($invalid_characters_used)) { + $form_state->setError($element, t('The %element-title is using the following invalid characters: @invalid-characters.', array('%element-title' => $title, '@invalid-characters' => implode(', ', $invalid_characters_used)))); + } + + if (preg_match('/(\s$)+/', $element['#value'])) { + $form_state->setError($element, t('The %element-title doesn\'t allow the patterns ending with whitespace.', array('%element-title' => $title))); + } + } + + return $element; + +} diff --git a/web/modules/contrib/pathauto/pathauto.permissions.yml b/web/modules/contrib/pathauto/pathauto.permissions.yml new file mode 100644 index 000000000..ede9f288c --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.permissions.yml @@ -0,0 +1,6 @@ +administer pathauto: + title: 'Administer pathauto' + description: 'Allows a user to configure patterns for automated aliases and bulk delete URL-aliases.' +notify of path changes: + title: 'Notify of Path Changes' + description: 'Determines whether or not users are notified.' diff --git a/web/modules/contrib/pathauto/pathauto.routing.yml b/web/modules/contrib/pathauto/pathauto.routing.yml new file mode 100644 index 000000000..e55063c5b --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.routing.yml @@ -0,0 +1,54 @@ +entity.pathauto_pattern.collection: + path: '/admin/config/search/path/patterns' + defaults: + _entity_list: 'pathauto_pattern' + _title: 'Patterns' + requirements: + _permission: 'administer pathauto' + +entity.pathauto_pattern.add_form: + path: '/admin/config/search/path/patterns/add' + defaults: + _entity_form: 'pathauto_pattern.default' + _title: 'Add Pathauto pattern' + tempstore_id: 'pathauto.pattern' + requirements: + _permission: 'administer pathauto' + +pathauto.settings.form: + path: '/admin/config/search/path/settings' + defaults: + _form: '\Drupal\pathauto\Form\PathautoSettingsForm' + _title: 'Settings' + requirements: + _permission: 'administer pathauto' + +entity.pathauto_pattern.enable: + path: '/admin/config/search/path/patterns/{pathauto_pattern}/enable' + defaults: + _entity_form: 'pathauto_pattern.enable' + requirements: + _entity_access: 'pathauto_pattern.update' + +entity.pathauto_pattern.disable: + path: '/admin/config/search/path/patterns/{pathauto_pattern}/disable' + defaults: + _entity_form: 'pathauto_pattern.disable' + requirements: + _entity_access: 'pathauto_pattern.update' + +pathauto.bulk.update.form: + path: '/admin/config/search/path/update_bulk' + defaults: + _form: '\Drupal\pathauto\Form\PathautoBulkUpdateForm' + _title: 'Bulk generate' + requirements: + _permission: 'administer url aliases' + +pathauto.admin.delete: + path: '/admin/config/search/path/delete_bulk' + defaults: + _form: '\Drupal\pathauto\Form\PathautoAdminDelete' + _title: 'Delete aliases' + requirements: + _permission: 'administer url aliases' diff --git a/web/modules/contrib/pathauto/pathauto.services.yml b/web/modules/contrib/pathauto/pathauto.services.yml new file mode 100644 index 000000000..3ce704ec7 --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.services.yml @@ -0,0 +1,26 @@ +services: + pathauto.generator: + class: Drupal\pathauto\PathautoGenerator + arguments: ['@config.factory', '@module_handler', '@token', '@pathauto.alias_cleaner', '@pathauto.alias_storage_helper', '@pathauto.alias_uniquifier', '@pathauto.verbose_messenger', '@string_translation', '@token.entity_mapper', '@entity_type.manager'] + pathauto.alias_cleaner: + class: Drupal\pathauto\AliasCleaner + arguments: ['@config.factory', '@pathauto.alias_storage_helper', '@language_manager', '@cache.discovery', '@transliteration', '@module_handler'] + pathauto.alias_storage_helper: + class: Drupal\pathauto\AliasStorageHelper + arguments: ['@config.factory', '@path.alias_storage', '@database','@pathauto.verbose_messenger', '@string_translation'] + tags: + - { name: backend_overridable } + pathauto.alias_uniquifier: + class: Drupal\pathauto\AliasUniquifier + arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.route_provider', '@path.alias_manager'] + pathauto.verbose_messenger: + class: Drupal\pathauto\VerboseMessenger + arguments: ['@config.factory', '@current_user'] + plugin.manager.alias_type: + class: Drupal\pathauto\AliasTypeManager + parent: default_plugin_manager + pathauto.settings_cache_tag: + class: Drupal\pathauto\EventSubscriber\PathautoSettingsCacheTag + arguments: ['@entity_field.manager', '@plugin.manager.alias_type'] + tags: + - { name: event_subscriber } diff --git a/web/modules/contrib/pathauto/pathauto.tokens.inc b/web/modules/contrib/pathauto/pathauto.tokens.inc new file mode 100644 index 000000000..626ff09c5 --- /dev/null +++ b/web/modules/contrib/pathauto/pathauto.tokens.inc @@ -0,0 +1,49 @@ + t('Joined path'), + 'description' => t('The array values each cleaned by Pathauto and then joined with the slash into a string that resembles an URL.'), + ); + + return $info; +} + +/** + * Implements hook_tokens(). + */ +function pathauto_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + $replacements = array(); + + if ($type == 'array' && !empty($data['array'])) { + $array = $data['array']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'join-path': + $values = array(); + foreach (token_element_children($array) as $key) { + $value = is_array($array[$key]) ? render($array[$key]) : (string) $array[$key]; + $value = \Drupal::service('pathauto.alias_cleaner')->cleanString($value, $options); + $values[] = $value; + } + $replacements[$original] = implode('/', $values); + break; + } + } + } + + return $replacements; +} diff --git a/web/modules/contrib/pathauto/src/AliasCleaner.php b/web/modules/contrib/pathauto/src/AliasCleaner.php new file mode 100644 index 000000000..340a26673 --- /dev/null +++ b/web/modules/contrib/pathauto/src/AliasCleaner.php @@ -0,0 +1,351 @@ +configFactory = $config_factory; + $this->aliasStorageHelper = $alias_storage_helper; + $this->languageManager = $language_manager; + $this->cacheBackend = $cache_backend; + $this->transliteration = $transliteration; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function cleanAlias($alias) { + $config = $this->configFactory->get('pathauto.settings'); + $alias_max_length = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()); + + $output = $alias; + + // Trim duplicate, leading, and trailing separators. Do this before cleaning + // backslashes since a pattern like "[token1]/[token2]-[token3]/[token4]" + // could end up like "value1/-/value2" and if backslashes were cleaned first + // this would result in a duplicate blackslash. + $output = $this->getCleanSeparators($output); + + // Trim duplicate, leading, and trailing backslashes. + $output = $this->getCleanSeparators($output, '/'); + + // Shorten to a logical place based on word boundaries. + $output = Unicode::truncate($output, $alias_max_length, TRUE); + + return $output; + } + + /** + * {@inheritdoc} + */ + public function getCleanSeparators($string, $separator = NULL) { + $config = $this->configFactory->get('pathauto.settings'); + + if (!isset($separator)) { + $separator = $config->get('separator'); + } + + $output = $string; + + if (strlen($separator)) { + // Trim any leading or trailing separators. + $output = trim($output, $separator); + + // Escape the separator for use in regular expressions. + $seppattern = preg_quote($separator, '/'); + + // Replace multiple separators with a single one. + $output = preg_replace("/$seppattern+/", $separator, $output); + + // Replace trailing separators around slashes. + if ($separator !== '/') { + $output = preg_replace("/\/+$seppattern\/+|$seppattern\/+|\/+$seppattern/", "/", $output); + } + else { + // If the separator is a slash, we need to re-add the leading slash + // dropped by the trim function. + $output = '/' . $output; + } + } + + return $output; + } + + /** + * {@inheritdoc} + */ + public function cleanString($string, array $options = array()) { + if (empty($this->cleanStringCache)) { + // Generate and cache variables used in this method. + $config = $this->configFactory->get('pathauto.settings'); + $this->cleanStringCache = array( + 'separator' => $config->get('separator'), + 'strings' => array(), + 'transliterate' => $config->get('transliterate'), + 'punctuation' => array(), + 'reduce_ascii' => (bool) $config->get('reduce_ascii'), + 'ignore_words_regex' => FALSE, + 'lowercase' => (bool) $config->get('case'), + 'maxlength' => min($config->get('max_component_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()), + ); + + // Generate and cache the punctuation replacements for strtr(). + $punctuation = $this->getPunctuationCharacters(); + foreach ($punctuation as $name => $details) { + $action = $config->get('punctuation.' . $name); + switch ($action) { + case PathautoGeneratorInterface::PUNCTUATION_REMOVE: + $this->cleanStringCache['punctuation'][$details['value']] = ''; + break; + + case PathautoGeneratorInterface::PUNCTUATION_REPLACE: + $this->cleanStringCache['punctuation'][$details['value']] = $this->cleanStringCache['separator']; + break; + + case PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING: + // Literally do nothing. + break; + } + } + + // Generate and cache the ignored words regular expression. + $ignore_words = $config->get('ignore_words'); + $ignore_words_regex = preg_replace(array('/^[,\s]+|[,\s]+$/', '/[,\s]+/'), array('', '\b|\b'), $ignore_words); + if ($ignore_words_regex) { + $this->cleanStringCache['ignore_words_regex'] = '\b' . $ignore_words_regex . '\b'; + if (function_exists('mb_eregi_replace')) { + mb_regex_encoding('UTF-8'); + $this->cleanStringCache['ignore_words_callback'] = 'mb_eregi_replace'; + } + else { + $this->cleanStringCache['ignore_words_callback'] = 'preg_replace'; + $this->cleanStringCache['ignore_words_regex'] = '/' . $this->cleanStringCache['ignore_words_regex'] . '/i'; + } + } + } + + // Empty strings do not need any processing. + if ($string === '' || $string === NULL) { + return ''; + } + + $langcode = NULL; + if (!empty($options['language'])) { + $langcode = $options['language']->getId(); + } + elseif (!empty($options['langcode'])) { + $langcode = $options['langcode']; + } + + // Check if the string has already been processed, and if so return the + // cached result. + if (isset($this->cleanStringCache['strings'][$langcode][(string) $string])) { + return $this->cleanStringCache['strings'][$langcode][(string) $string]; + } + + // Remove all HTML tags from the string. + $output = Html::decodeEntities($string); + $output = PlainTextOutput::renderFromHtml($output); + + // Optionally transliterate. + if ($this->cleanStringCache['transliterate']) { + // If the reduce strings to letters and numbers is enabled, don't bother + // replacing unknown characters with a question mark. Use an empty string + // instead. + $output = $this->transliteration->transliterate($output, $langcode, $this->cleanStringCache['reduce_ascii'] ? '' : '?'); + } + + // Replace or drop punctuation based on user settings. + $output = strtr($output, $this->cleanStringCache['punctuation']); + + // Reduce strings to letters and numbers. + if ($this->cleanStringCache['reduce_ascii']) { + $output = preg_replace('/[^a-zA-Z0-9\/]+/', $this->cleanStringCache['separator'], $output); + } + + // Get rid of words that are on the ignore list. + if ($this->cleanStringCache['ignore_words_regex']) { + $words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output); + if (Unicode::strlen(trim($words_removed)) > 0) { + $output = $words_removed; + } + } + + // Always replace whitespace with the separator. + $output = preg_replace('/\s+/', $this->cleanStringCache['separator'], $output); + + // Trim duplicates and remove trailing and leading separators. + $output = $this->getCleanSeparators($this->getCleanSeparators($output, $this->cleanStringCache['separator'])); + + // Optionally convert to lower case. + if ($this->cleanStringCache['lowercase']) { + $output = Unicode::strtolower($output); + } + + // Shorten to a logical place based on word boundaries. + $output = Unicode::truncate($output, $this->cleanStringCache['maxlength'], TRUE); + + // Cache this result in the static array. + $this->cleanStringCache['strings'][$langcode][(string) $string] = $output; + + return $output; + } + + + /** + * {@inheritdoc} + */ + public function getPunctuationCharacters() { + if (empty($this->punctuationCharacters)) { + $langcode = $this->languageManager->getCurrentLanguage()->getId(); + + $cid = 'pathauto:punctuation:' . $langcode; + if ($cache = $this->cacheBackend->get($cid)) { + $this->punctuationCharacters = $cache->data; + } + else { + $punctuation = array(); + $punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotation marks')); + $punctuation['quotes'] = array('value' => '\'', 'name' => t("Single quotation marks (apostrophe)")); + $punctuation['backtick'] = array('value' => '`', 'name' => t('Back tick')); + $punctuation['comma'] = array('value' => ',', 'name' => t('Comma')); + $punctuation['period'] = array('value' => '.', 'name' => t('Period')); + $punctuation['hyphen'] = array('value' => '-', 'name' => t('Hyphen')); + $punctuation['underscore'] = array('value' => '_', 'name' => t('Underscore')); + $punctuation['colon'] = array('value' => ':', 'name' => t('Colon')); + $punctuation['semicolon'] = array('value' => ';', 'name' => t('Semicolon')); + $punctuation['pipe'] = array('value' => '|', 'name' => t('Vertical bar (pipe)')); + $punctuation['left_curly'] = array('value' => '{', 'name' => t('Left curly bracket')); + $punctuation['left_square'] = array('value' => '[', 'name' => t('Left square bracket')); + $punctuation['right_curly'] = array('value' => '}', 'name' => t('Right curly bracket')); + $punctuation['right_square'] = array('value' => ']', 'name' => t('Right square bracket')); + $punctuation['plus'] = array('value' => '+', 'name' => t('Plus sign')); + $punctuation['equal'] = array('value' => '=', 'name' => t('Equal sign')); + $punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk')); + $punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand')); + $punctuation['percent'] = array('value' => '%', 'name' => t('Percent sign')); + $punctuation['caret'] = array('value' => '^', 'name' => t('Caret')); + $punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar sign')); + $punctuation['hash'] = array('value' => '#', 'name' => t('Number sign (pound sign, hash)')); + $punctuation['at'] = array('value' => '@', 'name' => t('At sign')); + $punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation mark')); + $punctuation['tilde'] = array('value' => '~', 'name' => t('Tilde')); + $punctuation['left_parenthesis'] = array('value' => '(', 'name' => t('Left parenthesis')); + $punctuation['right_parenthesis'] = array('value' => ')', 'name' => t('Right parenthesis')); + $punctuation['question_mark'] = array('value' => '?', 'name' => t('Question mark')); + $punctuation['less_than'] = array('value' => '<', 'name' => t('Less-than sign')); + $punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater-than sign')); + $punctuation['slash'] = array('value' => '/', 'name' => t('Slash')); + $punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash')); + + // Allow modules to alter the punctuation list and cache the result. + $this->moduleHandler->alter('pathauto_punctuation_chars', $punctuation); + $this->cacheBackend->set($cid, $punctuation); + $this->punctuationCharacters = $punctuation; + } + } + + return $this->punctuationCharacters; + } + + /** + * {@inheritdoc} + */ + public function cleanTokenValues(&$replacements, $data = array(), $options = array()) { + foreach ($replacements as $token => $value) { + // Only clean non-path tokens. + if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) { + $replacements[$token] = $this->cleanString($value, $options); + } + } + } + + /** + * {@inheritdoc} + */ + public function resetCaches() { + $this->cleanStringCache = array(); + } + +} diff --git a/web/modules/contrib/pathauto/src/AliasCleanerInterface.php b/web/modules/contrib/pathauto/src/AliasCleanerInterface.php new file mode 100644 index 000000000..87cc27e7d --- /dev/null +++ b/web/modules/contrib/pathauto/src/AliasCleanerInterface.php @@ -0,0 +1,103 @@ +configFactory = $config_factory; + $this->aliasStorage = $alias_storage; + $this->database = $database; + $this->messenger = $messenger; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function getAliasSchemaMaxLength() { + return $this->aliasSchemaMaxLength; + } + + /** + * {@inheritdoc} + */ + public function save(array $path, $existing_alias = NULL, $op = NULL) { + $config = $this->configFactory->get('pathauto.settings'); + + // Alert users if they are trying to create an alias that is the same as the + // internal path. + if ($path['source'] == $path['alias']) { + $this->messenger->addMessage($this->t('Ignoring alias %alias because it is the same as the internal path.', array('%alias' => $path['alias']))); + return NULL; + } + + // Skip replacing the current alias with an identical alias. + if (empty($existing_alias) || $existing_alias['alias'] != $path['alias']) { + $path += array( + 'pathauto' => TRUE, + 'original' => $existing_alias, + 'pid' => NULL, + ); + + // If there is already an alias, respect some update actions. + if (!empty($existing_alias)) { + switch ($config->get('update_action')) { + case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW: + // Do not create the alias. + return NULL; + + case PathautoGeneratorInterface::UPDATE_ACTION_LEAVE: + // Create a new alias instead of overwriting the existing by leaving + // $path['pid'] empty. + break; + + case PathautoGeneratorInterface::UPDATE_ACTION_DELETE: + // The delete actions should overwrite the existing alias. + $path['pid'] = $existing_alias['pid']; + break; + } + } + + // Save the path array. + $this->aliasStorage->save($path['source'], $path['alias'], $path['language'], $path['pid']); + + if (!empty($existing_alias['pid'])) { + $this->messenger->addMessage($this->t( + 'Created new alias %alias for %source, replacing %old_alias.', + array( + '%alias' => $path['alias'], + '%source' => $path['source'], + '%old_alias' => $existing_alias['alias'], + ) + ) + ); + } + else { + $this->messenger->addMessage($this->t('Created new alias %alias for %source.', array( + '%alias' => $path['alias'], + '%source' => $path['source'], + ))); + } + + return $path; + } + } + + /** + * {@inheritdoc} + */ + public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) { + $alias = $this->aliasStorage->load([ + 'source' => $source, + 'langcode' => $language, + ]); + // If no alias was fetched and if a language was specified, fallbacks to + // undefined language. + if (!$alias && ($language !== LanguageInterface::LANGCODE_NOT_SPECIFIED)) { + $alias = $this->aliasStorage->load([ + 'source' => $source, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + } + return $alias; + } + + /** + * {@inheritdoc} + */ + public function deleteBySourcePrefix($source) { + $pids = $this->loadBySourcePrefix($source); + if ($pids) { + $this->deleteMultiple($pids); + } + } + + /** + * {@inheritdoc} + */ + public function deleteAll() { + $this->database->truncate('url_alias')->execute(); + } + + /** + * {@inheritdoc} + */ + public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL) { + $this->deleteBySourcePrefix('/' . $entity->toUrl('canonical')->getInternalPath()); + if (isset($default_uri) && $entity->toUrl('canonical')->toString() != $default_uri) { + $this->deleteBySourcePrefix($default_uri); + } + } + + /** + * {@inheritdoc} + */ + public function loadBySourcePrefix($source) { + $select = $this->database->select('url_alias', 'u') + ->fields('u', array('pid')); + + $or_group = $select->orConditionGroup() + ->condition('source', $source) + ->condition('source', rtrim($source, '/') . '/%', 'LIKE'); + + return $select + ->condition($or_group) + ->execute() + ->fetchCol(); + } + + /** + * {@inheritdoc} + */ + public function countBySourcePrefix($source) { + $select = $this->database->select('url_alias', 'u') + ->fields('u', array('pid')); + + $or_group = $select->orConditionGroup() + ->condition('source', $source) + ->condition('source', rtrim($source, '/') . '/%', 'LIKE'); + + return $select + ->condition($or_group) + ->countQuery() + ->execute() + ->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function countAll() { + return $this->database->select('url_alias') + ->countQuery() + ->execute() + ->fetchField(); + } + + /** + * Delete multiple URL aliases. + * + * Intent of this is to abstract a potential path_delete_multiple() function + * for Drupal 7 or 8. + * + * @param int[] $pids + * An array of path IDs to delete. + */ + public function deleteMultiple($pids) { + foreach ($pids as $pid) { + $this->aliasStorage->delete(array('pid' => $pid)); + } + } + +} diff --git a/web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php b/web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php new file mode 100644 index 000000000..4f1788017 --- /dev/null +++ b/web/modules/contrib/pathauto/src/AliasStorageHelperInterface.php @@ -0,0 +1,117 @@ +alterInfo('pathauto_alias_types'); + $this->setCacheBackend($cache_backend, 'pathauto_alias_types'); + } + + /** + * Returns plugin definitions that support a given token type. + * + * @param string $type + * The type of token plugin must support to be useful. + * + * @return array + * Plugin definitions. + */ + public function getPluginDefinitionByType($type) { + $definitions = array_filter($this->getDefinitions(), function ($definition) use ($type) { + if (!empty($definition['types']) && in_array($type, $definition['types'])) { + return TRUE; + } + return FALSE; + }); + return $definitions; + } + + /** + * {@inheritdoc} + */ + public function getFallbackPluginId($plugin_id, array $configuration = array()) { + return 'broken'; + } + + /** + * Gets the definition of all visible plugins for this type. + * + * @return array + * An array of plugin definitions (empty array if no definitions were + * found). Keys are plugin IDs. + */ + public function getVisibleDefinitions() { + $definitions = $this->getDefinitions(); + unset($definitions['broken']); + return $definitions; + } + +} diff --git a/web/modules/contrib/pathauto/src/AliasUniquifier.php b/web/modules/contrib/pathauto/src/AliasUniquifier.php new file mode 100644 index 000000000..43fc5fa3f --- /dev/null +++ b/web/modules/contrib/pathauto/src/AliasUniquifier.php @@ -0,0 +1,167 @@ +configFactory = $config_factory; + $this->aliasStorageHelper = $alias_storage_helper; + $this->moduleHandler = $module_handler; + $this->routeProvider = $route_provider; + $this->aliasManager = $alias_manager; + } + + /** + * {@inheritdoc} + */ + public function uniquify(&$alias, $source, $langcode) { + $config = $this->configFactory->get('pathauto.settings'); + + if (!$this->isReserved($alias, $source, $langcode)) { + return; + } + + // If the alias already exists, generate a new, hopefully unique, variant. + $maxlength = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxlength()); + $separator = $config->get('separator'); + $original_alias = $alias; + + $i = 0; + do { + // Append an incrementing numeric suffix until we find a unique alias. + $unique_suffix = $separator . $i; + $alias = Unicode::truncate($original_alias, $maxlength - Unicode::strlen($unique_suffix), TRUE) . $unique_suffix; + $i++; + } while ($this->isReserved($alias, $source, $langcode)); + } + + /** + * {@inheritdoc} + */ + public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) { + // Check if this alias already exists. + if ($existing_source = $this->aliasManager->getPathByAlias($alias, $langcode)) { + if ($existing_source != $alias) { + // If it is an alias for the provided source, it is allowed to keep using + // it. If not, then it is reserved. + return $existing_source != $source; + } + + } + + // Then check if there is a route with the same path. + if ($this->isRoute($alias)) { + return TRUE; + } + // Finally check if any other modules have reserved the alias. + $args = array( + $alias, + $source, + $langcode, + ); + $implementations = $this->moduleHandler->getImplementations('pathauto_is_alias_reserved'); + foreach ($implementations as $module) { + + $result = $this->moduleHandler->invoke($module, 'pathauto_is_alias_reserved', $args); + + if (!empty($result)) { + // As soon as the first module says that an alias is in fact reserved, + // then there is no point in checking the rest of the modules. + return TRUE; + } + } + + return FALSE; + } + + /** + * Verify if the given path is a valid route. + * + * @param string $path + * A string containing a relative path. + * + * @return bool + * TRUE if the path already exists. + * + * @throws \InvalidArgumentException + */ + public function isRoute($path) { + if (is_file(DRUPAL_ROOT . '/' . $path) || is_dir(DRUPAL_ROOT . '/' . $path)) { + // Do not allow existing files or directories to get assigned an automatic + // alias. Note that we do not need to use is_link() to check for symbolic + // links since this returns TRUE for either is_file() or is_dir() already. + return TRUE; + } + + $routes = $this->routeProvider->getRoutesByPattern($path); + + // Only return true for an exact match, ignore placeholders. + foreach ($routes as $route) { + if ($route->getPath() == $path) { + return TRUE; + } + } + + return FALSE; + + } + +} diff --git a/web/modules/contrib/pathauto/src/AliasUniquifierInterface.php b/web/modules/contrib/pathauto/src/AliasUniquifierInterface.php new file mode 100644 index 000000000..2da4ba27a --- /dev/null +++ b/web/modules/contrib/pathauto/src/AliasUniquifierInterface.php @@ -0,0 +1,42 @@ +getSelectionConditions() as $id => $condition) { + $criteria[$id] = $condition->getConfiguration(); + } + $this->selection_criteria = $criteria; + + // Invalidate the static caches. + \Drupal::service('pathauto.generator')->resetCaches(); + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + // Invalidate the static caches. + \Drupal::service('pathauto.generator')->resetCaches(); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + $this->calculatePluginDependencies($this->getAliasType()); + + foreach ($this->getSelectionConditions() as $instance) { + $this->calculatePluginDependencies($instance); + } + + return $this->getDependencies(); + } + + /** + * {@inheritdoc} + */ + public function getPattern() { + return $this->pattern; + } + + /** + * {@inheritdoc} + */ + public function setPattern($pattern) { + $this->pattern = $pattern; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->type; + } + + /** + * {@inheritdoc} + */ + public function getAliasType() { + if (!$this->aliasTypeCollection) { + $this->aliasTypeCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.alias_type'), $this->getType(), ['default' => $this->getPattern()]); + } + return $this->aliasTypeCollection->get($this->getType()); + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->weight; + } + + /** + * {@inheritdoc} + */ + public function setWeight($weight) { + $this->weight = $weight; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + $contexts = $this->getAliasType()->getContexts(); + foreach ($this->getRelationships() as $token => $definition) { + /** @var \Drupal\ctools\TypedDataResolver $resolver */ + $resolver = \Drupal::service('ctools.typed_data.resolver'); + $context = $resolver->convertTokenToContext($token, $contexts); + $context_definition = $context->getContextDefinition(); + if (!empty($definition['label'])) { + $context_definition->setLabel($definition['label']); + } + $contexts[$token] = $context; + } + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function hasRelationship($token) { + return isset($this->relationships[$token]); + } + + /** + * {@inheritdoc} + */ + public function addRelationship($token, $label = NULL) { + if (!$this->hasRelationship($token)) { + $this->relationships[$token] = [ + 'label' => $label, + ]; + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function replaceRelationship($token, $label) { + if ($this->hasRelationship($token)) { + $this->relationships[$token] = [ + 'label' => $label, + ]; + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeRelationship($token) { + unset($this->relationships[$token]); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRelationships() { + return $this->relationships; + } + + /** + * {@inheritdoc} + */ + public function getSelectionConditions() { + if (!$this->selectionConditionCollection) { + $this->selectionConditionCollection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $this->get('selection_criteria')); + } + return $this->selectionConditionCollection; + } + + /** + * {@inheritdoc} + */ + public function addSelectionCondition(array $configuration) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + $this->getSelectionConditions()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * {@inheritdoc} + */ + public function getSelectionCondition($condition_id) { + return $this->getSelectionConditions()->get($condition_id); + } + + /** + * {@inheritdoc} + */ + public function removeSelectionCondition($condition_id) { + $this->getSelectionConditions()->removeInstanceId($condition_id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSelectionLogic() { + return $this->selection_logic; + } + + /** + * {@inheritdoc} + */ + public function applies($object) { + if ($this->getAliasType()->applies($object)) { + $definitions = $this->getAliasType()->getContextDefinitions(); + if (count($definitions) > 1) { + throw new \Exception("Alias types do not support more than one context."); + } + $keys = array_keys($definitions); + // Set the context object on our Alias plugin before retrieving contexts. + $this->getAliasType()->setContextValue($keys[0], $object); + /** @var \Drupal\Core\Plugin\Context\ContextInterface[] $base_contexts */ + $contexts = $this->getContexts(); + /** @var \Drupal\Core\Plugin\Context\ContextHandler $context_handler */ + $context_handler = \Drupal::service('context.handler'); + $conditions = $this->getSelectionConditions(); + foreach ($conditions as $condition) { + if ($condition instanceof ContextAwarePluginInterface) { + try { + $context_handler->applyContextMapping($condition, $contexts); + } + catch (ContextException $e) { + watchdog_exception('pathauto', $e); + return FALSE; + } + } + $result = $condition->execute(); + if ($this->getSelectionLogic() == 'and' && !$result) { + return FALSE; + } + elseif ($this->getSelectionLogic() == 'or' && $result) { + return TRUE; + } + } + return TRUE; + } + return FALSE; + } + +} diff --git a/web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php b/web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php new file mode 100644 index 000000000..371fe2e2f --- /dev/null +++ b/web/modules/contrib/pathauto/src/EventSubscriber/PathautoSettingsCacheTag.php @@ -0,0 +1,54 @@ +entityFieldManager = $entity_field_manager; + $this->aliasTypeManager = $alias_type_manager; + } + + /** + * 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) { + if ($event->getConfig()->getName() === 'pathauto.settings') { + $config = $event->getConfig(); + $original_entity_types = $config->getOriginal('enabled_entity_types'); + + // Clear cached field definitions if the values are changed. + if ($original_entity_types != $config->get('enabled_entity_types')) { + $this->entityFieldManager->clearCachedFieldDefinitions(); + $this->aliasTypeManager->clearCachedDefinitions(); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[ConfigEvents::SAVE][] = ['onSave']; + return $events; + } + +} diff --git a/web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php b/web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php new file mode 100644 index 000000000..a23052520 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Form/PathautoAdminDelete.php @@ -0,0 +1,185 @@ +aliasTypeManager = $alias_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.alias_type') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'pathauto_admin_delete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['delete'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Choose aliases to delete'), + '#tree' => TRUE, + ]; + + // First we do the "all" case. + $storage_helper = \Drupal::service('pathauto.alias_storage_helper'); + $total_count = $storage_helper->countAll(); + $form['delete']['all_aliases'] = [ + '#type' => 'checkbox', + '#title' => $this->t('All aliases'), + '#default_value' => FALSE, + '#description' => $this->t('Delete all aliases. Number of aliases which will be deleted: %count.', ['%count' => $total_count]), + ]; + + // Next, iterate over all visible alias types. + $definitions = $this->aliasTypeManager->getVisibleDefinitions(); + + foreach ($definitions as $id => $definition) { + /** @var \Drupal\pathauto\AliasTypeInterface $alias_type */ + $alias_type = $this->aliasTypeManager->createInstance($id); + $count = $storage_helper->countBySourcePrefix($alias_type->getSourcePrefix()); + $form['delete']['plugins'][$id] = [ + '#type' => 'checkbox', + '#title' => (string) $definition['label'], + '#default_value' => FALSE, + '#description' => $this->t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', ['@label' => (string) $definition['label'], '%count' => $count]), + ]; + } + + $form['options'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Delete options'), + '#tree' => TRUE, + ]; + + // Provide checkbox for not deleting custom aliases. + $form['options']['keep_custom_aliases'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Only delete automatically generated aliases'), + '#default_value' => TRUE, + '#description' => $this->t('When checked, aliases which have been manually set are not affected by this mass-deletion.'), + ]; + + // Warn them and give a button that shows we mean business. + $form['warning'] = ['#value' => '

' . $this->t('Note: there is no confirmation. Be sure of your action before clicking the "Delete aliases now!" button.
You may want to make a backup of the database and/or the url_alias table prior to using this feature.') . '

']; + $form['buttons']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete aliases now!'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $delete_all = $form_state->getValue(['delete', 'all_aliases']); + // Keeping custom aliases forces us to go the slow way to correctly check + // the automatic/manual flag. + if ($form_state->getValue(['options', 'keep_custom_aliases'])) { + $batch = [ + 'title' => $this->t('Bulk deleting URL aliases'), + 'operations' => [['Drupal\pathauto\Form\PathautoAdminDelete::batchStart', [$delete_all]]], + 'finished' => 'Drupal\pathauto\Form\PathautoAdminDelete::batchFinished', + ]; + + if ($delete_all) { + foreach (array_keys($form_state->getValue(['delete', 'plugins'])) as $id) { + $batch['operations'][] = ['Drupal\pathauto\Form\PathautoAdminDelete::batchProcess', [$id]]; + } + } + else { + foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) { + $batch['operations'][] = ['Drupal\pathauto\Form\PathautoAdminDelete::batchProcess', [$id]]; + } + } + + batch_set($batch); + } + else if ($delete_all) { + \Drupal::service('pathauto.alias_storage_helper')->deleteAll(); + drupal_set_message($this->t('All of your path aliases have been deleted.')); + } + else { + $storage_helper = \Drupal::service('pathauto.alias_storage_helper'); + foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) { + $alias_type = $this->aliasTypeManager->createInstance($id); + $storage_helper->deleteBySourcePrefix((string) $alias_type->getSourcePrefix()); + drupal_set_message($this->t('All of your %label path aliases have been deleted.', ['%label' => $alias_type->getLabel()])); + } + } + } + + /** + * Batch callback; record if aliases of all types must be deleted. + */ + public static function batchStart($delete_all, &$context) { + $context['results']['delete_all'] = $delete_all; + $context['results']['deletions'] = []; + } + + /** + * Common batch processing callback for all operations. + */ + public static function batchProcess($id, &$context) { + /** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */ + $alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id); + $alias_type->batchDelete($context); + } + + /** + * Batch finished callback. + */ + public static function batchFinished($success, $results, $operations) { + if ($success) { + if ($results['delete_all']) { + drupal_set_message(t('All of your automatically generated path aliases have been deleted.')); + } + else if (isset($results['deletions'])) { + foreach (array_values($results['deletions']) as $label) { + drupal_set_message(t('All of your automatically generated %label path aliases have been deleted.', ['%label' => $label])); + } + } + } + else { + $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)))); + } + } + +} diff --git a/web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php b/web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php new file mode 100644 index 000000000..e66cd9df5 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Form/PathautoBulkUpdateForm.php @@ -0,0 +1,164 @@ +aliasTypeManager = $alias_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.alias_type') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'pathauto_bulk_update_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $form = array(); + + $form['#update_callbacks'] = array(); + + $form['update'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Select the types of paths for which to generate URL aliases'), + '#options' => array(), + '#default_value' => array(), + ); + + $definitions = $this->aliasTypeManager->getVisibleDefinitions(); + + foreach ($definitions as $id => $definition) { + $alias_type = $this->aliasTypeManager->createInstance($id); + if ($alias_type instanceof AliasTypeBatchUpdateInterface) { + $form['update']['#options'][$id] = $alias_type->getLabel(); + } + } + + $form['action'] = array( + '#type' => 'radios', + '#title' => $this->t('Select which URL aliases to generate'), + '#options' => ['create' => $this->t('Generate a URL alias for un-aliased paths only')], + '#default_value' => 'create', + ); + + $config = $this->config('pathauto.settings'); + + if ($config->get('update_action') == PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW) { + // Existing aliases should not be updated. + $form['warning'] = array( + '#markup' => $this->t('Pathauto settings are set to ignore paths which already have a URL alias. You can only create URL aliases for paths having none.', [':url' => Url::fromRoute('pathauto.settings.form')->toString()]), + ); + } + else { + $form['action']['#options']['update'] = $this->t('Update the URL alias for paths having an old URL alias'); + $form['action']['#options']['all'] = $this->t('Regenerate URL aliases for all paths'); + } + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Update'), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $batch = array( + 'title' => $this->t('Bulk updating URL aliases'), + 'operations' => array( + array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchStart', array()), + ), + 'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished', + ); + + $action = $form_state->getValue('action'); + + foreach ($form_state->getValue('update') as $id) { + if (!empty($id)) { + $batch['operations'][] = array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', [$id, $action]); + } + } + + batch_set($batch); + } + + /** + * Batch callback; initialize the number of updated aliases. + */ + public static function batchStart(&$context) { + $context['results']['updates'] = 0; + } + + /** + * Common batch processing callback for all operations. + * + * Required to load our include the proper batch file. + */ + public static function batchProcess($id, $action, &$context) { + /** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */ + $alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id); + $alias_type->batchUpdate($action, $context); + } + + /** + * Batch finished callback. + */ + public static function batchFinished($success, $results, $operations) { + if ($success) { + if ($results['updates']) { + drupal_set_message(\Drupal::translation()->formatPlural($results['updates'], 'Generated 1 URL alias.', 'Generated @count URL aliases.')); + } + else { + drupal_set_message(t('No new URL aliases to generate.')); + } + } + else { + $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)))); + } + } + +} diff --git a/web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php b/web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php new file mode 100644 index 000000000..281edb248 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Form/PathautoSettingsForm.php @@ -0,0 +1,266 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->aliasTypeManager = $alias_type_manager; + } + + /** + * {@inheritDoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('plugin.manager.alias_type') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'pathauto_settings_form'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['pathauto.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('pathauto.settings'); + + $form['enabled_entity_types'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Enabled entity types'), + '#description' => $this->t('Enable to add a path field and allow to define alias patterns for the given type. Disabled types already define a path field themselves or currently have a pattern.'), + '#tree' => TRUE, + ]; + + // Get all applicable entity types. + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + // Disable a checkbox if it already exists and if the entity type has + // patterns currently defined or if it isn't defined by us. + $patterns_count = \Drupal::entityQuery('pathauto_pattern') + ->condition('type', 'canonical_entities:' . $entity_type_id) + ->count() + ->execute(); + + if (is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical')) { + $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); + $form['enabled_entity_types'][$entity_type_id] = [ + '#type' => 'checkbox', + '#title' => $entity_type->getLabel(), + '#default_value' => isset($field_definitions['path']) || in_array($entity_type_id, $config->get('enabled_entity_types')), + '#disabled' => isset($field_definitions['path']) && ($field_definitions['path']->getProvider() != 'pathauto' || $patterns_count), + ]; + } + } + + $form['verbose'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Verbose'), + '#default_value' => $config->get('verbose'), + '#description' => $this->t('Display alias changes (except during bulk updates).'), + ); + + $form['separator'] = array( + '#type' => 'textfield', + '#title' => $this->t('Separator'), + '#size' => 1, + '#maxlength' => 1, + '#default_value' => $config->get('separator'), + '#description' => $this->t('Character used to separate words in titles. This will replace any spaces and punctuation characters. Using a space or + character can cause unexpected results.'), + ); + + $form['case'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Character case'), + '#default_value' => $config->get('case'), + '#description' => $this->t('Convert token values to lowercase.'), + ); + + $max_length = \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength(); + + $help_link = ''; + if (\Drupal::moduleHandler()->moduleExists('help')) { + $help_link = ' ' . $this->t('See Pathauto help for details.', [':pathauto-help' => Url::fromRoute('help.page', ['name' => 'pathauto'])->toString()]); + } + + $form['max_length'] = array( + '#type' => 'number', + '#title' => $this->t('Maximum alias length'), + '#size' => 3, + '#maxlength' => 3, + '#default_value' => $config->get('max_length'), + '#min' => 1, + '#max' => $max_length, + '#description' => $this->t('Maximum length of aliases to generate. 100 is the recommended length. @max is the maximum possible length.', array('@max' => $max_length)) . $help_link, + ); + + $form['max_component_length'] = array( + '#type' => 'number', + '#title' => $this->t('Maximum component length'), + '#size' => 3, + '#maxlength' => 3, + '#default_value' => $config->get('max_component_length'), + '#min' => 1, + '#max' => $max_length, + '#description' => $this->t('Maximum text length of any component in the alias (e.g., [title]). 100 is the recommended length. @max is the maximum possible length.', ['@max' => $max_length]) . $help_link, + ); + + $description = $this->t('What should Pathauto do when updating an existing content item which already has an alias?'); + if (\Drupal::moduleHandler()->moduleExists('redirect')) { + $description .= ' ' . $this->t('The Redirect module settings affect whether a redirect is created when an alias is deleted.', array(':url' => Url::fromRoute('redirect.settings')->toString())); + } + else { + $description .= ' ' . $this->t('Considering installing the Redirect module to get redirects when your aliases change.', array(':url' => 'http://drupal.org/project/redirect')); + } + + $form['update_action'] = array( + '#type' => 'radios', + '#title' => $this->t('Update action'), + '#default_value' => $config->get('update_action'), + '#options' => array( + PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW => $this->t('Do nothing. Leave the old alias intact.'), + PathautoGeneratorInterface::UPDATE_ACTION_LEAVE => $this->t('Create a new alias. Leave the existing alias functioning.'), + PathautoGeneratorInterface::UPDATE_ACTION_DELETE => $this->t('Create a new alias. Delete the old alias.'), + ), + '#description' => $description, + ); + + $form['transliterate'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Transliterate prior to creating alias'), + '#default_value' => $config->get('transliterate'), + '#description' => $this->t('When a pattern includes certain characters (such as those with accents) should Pathauto attempt to transliterate them into the US-ASCII alphabet?'), + ); + + $form['reduce_ascii'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Reduce strings to letters and numbers'), + '#default_value' => $config->get('reduce_ascii'), + '#description' => $this->t('Filters the new alias to only letters and numbers found in the ASCII-96 set.'), + ); + + $form['ignore_words'] = array( + '#type' => 'textarea', + '#title' => $this->t('Strings to Remove'), + '#default_value' => $config->get('ignore_words'), + '#description' => $this->t('Words to strip out of the URL alias, separated by commas. Do not use this to remove punctuation.'), + '#wysiwyg' => FALSE, + ); + + $form['punctuation'] = array( + '#type' => 'fieldset', + '#title' => $this->t('Punctuation'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + ); + + $punctuation = \Drupal::service('pathauto.alias_cleaner')->getPunctuationCharacters(); + + foreach ($punctuation as $name => $details) { + // Use the value from config if it exists. + if ($config->get('punctuation.' . $name) !== NULL) { + $details['default'] = $config->get('punctuation.' . $name); + } + else { + // Otherwise use the correct default. + $details['default'] = $details['value'] == $config->get('separator') ? PathautoGeneratorInterface::PUNCTUATION_REPLACE : PathautoGeneratorInterface::PUNCTUATION_REMOVE; + } + $form['punctuation'][$name] = array( + '#type' => 'select', + '#title' => $details['name'] . ' (' . Html::escape($details['value']) . ')', + '#default_value' => $details['default'], + '#options' => array( + PathautoGeneratorInterface::PUNCTUATION_REMOVE => $this->t('Remove'), + PathautoGeneratorInterface::PUNCTUATION_REPLACE => $this->t('Replace by separator'), + PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING => $this->t('No action (do not replace)'), + ), + ); + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $config = $this->config('pathauto.settings'); + + $form_state->cleanValues(); + + $original_entity_types = $config->get('enabled_entity_types'); + foreach ($form_state->getValues() as $key => $value) { + if ($key == 'enabled_entity_types') { + $enabled_entity_types = []; + foreach ($value as $entity_type_id => $enabled) { + $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); + // Verify that the entity type is enabled and that it is not defined + // or defined by us before adding it to the configuration, so that + // we do not store an entity type that cannot be enabled or disabled. + if ($enabled && (!isset($field_definitions['path']) || ($field_definitions['path']->getProvider() === 'pathauto'))) { + $enabled_entity_types[] = $entity_type_id; + } + } + $value = $enabled_entity_types; + } + $config->set($key, $value); + } + $config->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/contrib/pathauto/src/Form/PatternDisableForm.php b/web/modules/contrib/pathauto/src/Form/PatternDisableForm.php new file mode 100644 index 000000000..c87b88b6a --- /dev/null +++ b/web/modules/contrib/pathauto/src/Form/PatternDisableForm.php @@ -0,0 +1,52 @@ +t('Are you sure you want to disable the pattern %label?', array('%label' => $this->entity->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.pathauto_pattern.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Disable'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('Disabled patterns are ignored when generating aliases.'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->disable()->save(); + drupal_set_message($this->t('Disabled pattern %label.', array('%label' => $this->entity->label()))); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/contrib/pathauto/src/Form/PatternEditForm.php b/web/modules/contrib/pathauto/src/Form/PatternEditForm.php new file mode 100644 index 000000000..49efc7e77 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Form/PatternEditForm.php @@ -0,0 +1,285 @@ +get('plugin.manager.alias_type'), + $container->get('entity_type.bundle.info'), + $container->get('entity_type.manager'), + $container->get('language_manager') + ); + } + + /** + * PatternEditForm constructor. + * + * @param \Drupal\pathauto\AliasTypeManager $manager + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + */ + function __construct(AliasTypeManager $manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) { + $this->manager = $manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->entityTypeManager = $entity_type_manager; + $this->languageManager = $language_manager; + } + + /** + * {@inheritDoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $options = []; + foreach ($this->manager->getVisibleDefinitions() as $plugin_id => $plugin_definition) { + $options[$plugin_id] = $plugin_definition['label']; + } + $form['type'] = [ + '#type' => 'select', + '#title' => $this->t('Pattern type'), + '#default_value' => $this->entity->getType(), + '#options' => $options, + '#required' => TRUE, + '#limit_validation_errors' => array(array('type')), + '#submit' => array('::submitSelectType'), + '#executes_submit_callback' => TRUE, + '#ajax' => array( + 'callback' => '::ajaxReplacePatternForm', + 'wrapper' => 'pathauto-pattern', + 'method' => 'replace', + ), + ]; + + $form['pattern_container'] = [ + '#type' => 'container', + '#prefix' => '
', + '#suffix' => '
', + ]; + + // if there is no type yet, stop here. + if ($this->entity->getType()) { + + $alias_type = $this->entity->getAliasType(); + + $form['pattern_container']['pattern'] = array( + '#type' => 'textfield', + '#title' => 'Path pattern', + '#default_value' => $this->entity->getPattern(), + '#size' => 65, + '#maxlength' => 1280, + '#element_validate' => array('token_element_validate', 'pathauto_pattern_validate'), + '#after_build' => array('token_element_validate'), + '#token_types' => $alias_type->getTokenTypes(), + '#min_tokens' => 1, + '#required' => TRUE, + ); + + // Show the token help relevant to this pattern type. + $form['pattern_container']['token_help'] = array( + '#theme' => 'token_tree_link', + '#token_types' => $alias_type->getTokenTypes(), + ); + + // Expose bundle and language conditions. + if ($alias_type->getDerivativeId() && $entity_type = $this->entityTypeManager->getDefinition($alias_type->getDerivativeId())) { + + $default_bundles = []; + $default_languages = []; + foreach ($this->entity->getSelectionConditions() as $condition_id => $condition) { + if (in_array($condition->getPluginId(), ['entity_bundle:' . $entity_type->id(), 'node_type'])) { + $default_bundles = $condition->getConfiguration()['bundles']; + } + elseif ($condition->getPluginId() == 'language') { + $default_languages = $condition->getConfiguration()['langcodes']; + } + } + + if ($entity_type->hasKey('bundle') && $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type->id())) { + $bundle_options = []; + foreach ($bundles as $id => $info) { + $bundle_options[$id] = $info['label']; + } + $form['pattern_container']['bundles'] = array( + '#title' => $entity_type->getBundleLabel(), + '#type' => 'checkboxes', + '#options' => $bundle_options, + '#default_value' => $default_bundles, + '#description' => $this->t('Check to which types this pattern should be applied. Leave empty to allow any.'), + ); + } + + if ($this->languageManager->isMultilingual() && $entity_type->isTranslatable()) { + $language_options = []; + foreach ($this->languageManager->getLanguages() as $id => $language) { + $language_options[$id] = $language->getName(); + } + $form['pattern_container']['languages'] = array( + '#title' => $this->t('Languages'), + '#type' => 'checkboxes', + '#options' => $language_options, + '#default_value' => $default_languages, + '#description' => $this->t('Check to which languages this pattern should be applied. Leave empty to allow any.'), + ); + } + } + } + + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $this->entity->label(), + '#required' => TRUE, + '#description' => $this->t('A short name to help you identify this pattern in the patterns list.'), + ); + + $form['id'] = array( + '#type' => 'machine_name', + '#title' => $this->t('ID'), + '#maxlength' => 255, + '#default_value' => $this->entity->id(), + '#required' => TRUE, + '#disabled' => !$this->entity->isNew(), + '#machine_name' => array( + 'exists' => 'Drupal\pathauto\Entity\PathautoPattern::load', + ), + ); + + $form['status'] = [ + '#title' => $this->t('Enabled'), + '#type' => 'checkbox', + '#default_value' => $this->entity->status(), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritDoc} + */ + public function buildEntity(array $form, FormStateInterface $form_state) { + /** @var \Drupal\pathauto\PathautoPatternInterface $entity */ + $entity = parent::buildEntity($form, $form_state); + + // Will only be used for new patterns. + $default_weight = 0; + + $alias_type = $entity->getAliasType(); + if ($alias_type->getDerivativeId() && $this->entityTypeManager->hasDefinition($alias_type->getDerivativeId())) { + $entity_type = $alias_type->getDerivativeId(); + // First, remove bundle and language conditions. + foreach ($entity->getSelectionConditions() as $condition_id => $condition) { + if (in_array($condition->getPluginId(), ['entity_bundle:' . $entity_type, 'node_type', 'language'])) { + $entity->removeSelectionCondition($condition_id); + } + } + + if ($bundles = array_filter((array) $form_state->getValue('bundles'))) { + $default_weight -= 5; + $plugin_id = $entity_type == 'node' ? 'node_type' : 'entity_bundle:' . $entity_type; + $entity->addSelectionCondition( + [ + 'id' => $plugin_id, + 'bundles' => $bundles, + 'negate' => FALSE, + 'context_mapping' => [ + $entity_type => $entity_type, + ] + ] + ); + } + + if ($languages = array_filter((array) $form_state->getValue('languages'))) { + $default_weight -= 5; + $language_mapping = $entity_type . ':' . $this->entityTypeManager->getDefinition($entity_type)->getKey('langcode') . ':language'; + $entity->addSelectionCondition( + [ + 'id' => 'language', + 'langcodes' => array_combine($languages, $languages), + 'negate' => FALSE, + 'context_mapping' => [ + 'language' => $language_mapping, + ] + ] + ); + $entity->addRelationship($language_mapping, t('Language')); + } + + } + + if ($entity->isNew()) { + $entity->setWeight($default_weight); + } + + return $entity; + } + + /** + * {@inheritDoc} + */ + public function save(array $form, FormStateInterface $form_state) { + parent::save($form, $form_state); + drupal_set_message($this->t('Pattern @label saved.', ['@label' => $this->entity->label()])); + $form_state->setRedirectUrl($this->entity->toUrl('collection')); + } + + /** + * Handles switching the type selector. + */ + public function ajaxReplacePatternForm($form, FormStateInterface $form_state) { + return $form['pattern_container']; + } + + /** + * Handles submit call when alias type is selected. + */ + public function submitSelectType(array $form, FormStateInterface $form_state) { + $this->entity = $this->buildEntity($form, $form_state); + $form_state->setRebuild(); + } + +} diff --git a/web/modules/contrib/pathauto/src/Form/PatternEnableForm.php b/web/modules/contrib/pathauto/src/Form/PatternEnableForm.php new file mode 100644 index 000000000..253ca319e --- /dev/null +++ b/web/modules/contrib/pathauto/src/Form/PatternEnableForm.php @@ -0,0 +1,52 @@ +t('Are you sure you want to enable the pattern %label?', array('%label' => $this->entity->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.pathauto_pattern.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Enable'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->enable()->save(); + drupal_set_message($this->t('Enabled pattern %label.', array('%label' => $this->entity->label()))); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/contrib/pathauto/src/MessengerInterface.php b/web/modules/contrib/pathauto/src/MessengerInterface.php new file mode 100644 index 000000000..e31f48fac --- /dev/null +++ b/web/modules/contrib/pathauto/src/MessengerInterface.php @@ -0,0 +1,20 @@ +configFactory = $config_factory; + $this->moduleHandler = $module_handler; + $this->token = $token; + $this->aliasCleaner = $alias_cleaner; + $this->aliasStorageHelper = $alias_storage_helper; + $this->aliasUniquifier = $alias_uniquifier; + $this->messenger = $messenger; + $this->stringTranslation = $string_translation; + $this->tokenEntityMapper = $token_entity_mappper; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function createEntityAlias(EntityInterface $entity, $op) { + // Retrieve and apply the pattern for this content type. + $pattern = $this->getPatternByEntity($entity); + if (empty($pattern)) { + // No pattern? Do nothing (otherwise we may blow away existing aliases...) + return NULL; + } + + $source = '/' . $entity->toUrl()->getInternalPath(); + $config = $this->configFactory->get('pathauto.settings'); + $langcode = $entity->language()->getId(); + + // Core does not handle aliases with language Not Applicable. + if ($langcode == LanguageInterface::LANGCODE_NOT_APPLICABLE) { + $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED; + } + + // Build token data. + $data = [ + $this->tokenEntityMapper->getTokenTypeForEntityType($entity->getEntityTypeId()) => $entity, + ]; + + // Allow other modules to alter the pattern. + $context = array( + 'module' => $entity->getEntityType()->getProvider(), + 'op' => $op, + 'source' => $source, + 'data' => $data, + 'bundle' => $entity->bundle(), + 'language' => &$langcode, + ); + // @todo Is still hook still useful? + $this->moduleHandler->alter('pathauto_pattern', $pattern, $context); + + // Special handling when updating an item which is already aliased. + $existing_alias = NULL; + if ($op == 'update' || $op == 'bulkupdate') { + if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $langcode)) { + switch ($config->get('update_action')) { + case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW: + // If an alias already exists, + // and the update action is set to do nothing, + // then gosh-darn it, do nothing. + return NULL; + } + } + } + + // Replace any tokens in the pattern. + // Uses callback option to clean replacements. No sanitization. + // Pass empty BubbleableMetadata object to explicitly ignore cacheablity, + // as the result is never rendered. + $alias = $this->token->replace($pattern->getPattern(), $data, array( + 'clear' => TRUE, + 'callback' => array($this->aliasCleaner, 'cleanTokenValues'), + 'langcode' => $langcode, + 'pathauto' => TRUE, + ), new BubbleableMetadata()); + + // Check if the token replacement has not actually replaced any values. If + // that is the case, then stop because we should not generate an alias. + // @see token_scan() + $pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern->getPattern()); + if ($alias === $pattern_tokens_removed) { + return NULL; + } + + $alias = $this->aliasCleaner->cleanAlias($alias); + + // Allow other modules to alter the alias. + $context['source'] = &$source; + $context['pattern'] = $pattern; + $this->moduleHandler->alter('pathauto_alias', $alias, $context); + + // If we have arrived at an empty string, discontinue. + if (!Unicode::strlen($alias)) { + return NULL; + } + + // If the alias already exists, generate a new, hopefully unique, variant. + $original_alias = $alias; + $this->aliasUniquifier->uniquify($alias, $source, $langcode); + if ($original_alias != $alias) { + // Alert the user why this happened. + $this->messenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', array( + '%original_alias' => $original_alias, + '%alias' => $alias, + )), $op); + } + + // Return the generated alias if requested. + if ($op == 'return') { + return $alias; + } + + // Build the new path alias array and send it off to be created. + $path = array( + 'source' => $source, + 'alias' => $alias, + 'language' => $langcode, + ); + + return $this->aliasStorageHelper->save($path, $existing_alias, $op); + } + + /** + * Loads pathauto patterns for a given entity type ID + * + * @param string $entity_type_id + * An entity type ID. + * + * @return \Drupal\pathauto\PathautoPatternInterface[] + * A list of patterns, sorted by weight. + */ + protected function getPatternByEntityType($entity_type_id) { + if (!isset($this->patternsByEntityType[$entity_type_id])) { + $ids = \Drupal::entityQuery('pathauto_pattern') + ->condition('type', array_keys(\Drupal::service('plugin.manager.alias_type') + ->getPluginDefinitionByType($this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id)))) + ->condition('status', 1) + ->sort('weight') + ->execute(); + + $this->patternsByEntityType[$entity_type_id] = \Drupal::entityTypeManager() + ->getStorage('pathauto_pattern') + ->loadMultiple($ids); + } + + return $this->patternsByEntityType[$entity_type_id]; + } + + /** + * {@inheritdoc} + */ + public function getPatternByEntity(EntityInterface $entity) { + $langcode = $entity->language()->getId(); + if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) { + foreach ($this->getPatternByEntityType($entity->getEntityTypeId()) as $pattern) { + if ($pattern->applies($entity)) { + $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = $pattern; + break; + } + } + // If still not set. + if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) { + $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = NULL; + } + } + return $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode]; + } + + /** + * {@inheritdoc} + */ + public function resetCaches() { + $this->patterns = []; + $this->patternsByEntityType = []; + $this->aliasCleaner->resetCaches(); + } + + /** + * {@inheritdoc} + */ + public function updateEntityAlias(EntityInterface $entity, $op, array $options = array()) { + // Skip if the entity does not have the path field. + if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) { + return NULL; + } + + // Skip if pathauto processing is disabled. + if ($entity->path->pathauto != PathautoState::CREATE && empty($options['force'])) { + return NULL; + } + + // Only act if this is the default revision. + if ($entity instanceof RevisionableInterface && !$entity->isDefaultRevision()) { + return NULL; + } + + $options += array('language' => $entity->language()->getId()); + $type = $entity->getEntityTypeId(); + + // Skip processing if the entity has no pattern. + if (!$this->getPatternByEntity($entity)) { + return NULL; + } + + // Deal with taxonomy specific logic. + // @todo Update and test forum related code. + if ($type == 'taxonomy_term') { + + $config_forum = $this->configFactory->get('forum.settings'); + if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) { + $type = 'forum'; + } + } + + try { + $result = $this->createEntityAlias($entity, $op); + } + catch (\InvalidArgumentException $e) { + drupal_set_message($e->getMessage(), 'error'); + return NULL; + } + + // @todo Move this to a method on the pattern plugin. + if ($type == 'taxonomy_term') { + foreach ($this->loadTermChildren($entity->id()) as $subterm) { + $this->updateEntityAlias($subterm, $op, $options); + } + } + + return $result; + } + + /** + * Finds all children of a term ID. + * + * @param int $tid + * Term ID to retrieve parents for. + * + * @return \Drupal\taxonomy\TermInterface[] + * An array of term objects that are the children of the term $tid. + */ + protected function loadTermChildren($tid) { + return $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren($tid); + } + +} diff --git a/web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php b/web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php new file mode 100644 index 000000000..5880adbc6 --- /dev/null +++ b/web/modules/contrib/pathauto/src/PathautoGeneratorInterface.php @@ -0,0 +1,89 @@ +setLabel(t('Pathauto state')) + ->setDescription(t('Whether an automated alias should be created or not.')) + ->setComputed(TRUE) + ->setClass('\Drupal\pathauto\PathautoState'); + return $properties; + } + + /** + * {@inheritdoc} + */ + public function postSave($update) { + // Only allow the parent implementation to act if pathauto will not create + // an alias. + if ($this->pathauto == PathautoState::SKIP) { + parent::postSave($update); + } + $this->get('pathauto')->persist(); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + // Make sure that the pathauto state flag does not get lost if just that is + // changed. + return !$this->alias && !$this->get('pathauto')->hasValue(); + } + + /** + * {@inheritdoc} + */ + public function applyDefaultValue($notify = TRUE) { + parent::applyDefaultValue($notify); + // Created fields default creating a new alias. + $this->setValue(array('pathauto' => PathautoState::CREATE), $notify); + return $this; + } + +} diff --git a/web/modules/contrib/pathauto/src/PathautoPatternInterface.php b/web/modules/contrib/pathauto/src/PathautoPatternInterface.php new file mode 100644 index 000000000..9782f2e05 --- /dev/null +++ b/web/modules/contrib/pathauto/src/PathautoPatternInterface.php @@ -0,0 +1,181 @@ +t('Label'); + $header['pattern'] = $this->t('Pattern'); + $header['type'] = $this->t('Pattern type'); + $header['conditions'] = $this->t('Conditions'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /* @var \Drupal\pathauto\PathautoPatternInterface $entity */ + $row['label'] = $entity->label(); + $row['patern']['#markup'] = $entity->getPattern(); + $row['type']['#markup'] = $entity->getAliasType()->getLabel(); + $row['conditions']['#theme'] = 'item_list'; + foreach ($entity->getSelectionConditions() as $condition) { + $row['conditions']['#items'][] = $condition->summary(); + } + return $row + parent::buildRow($entity); + } + +} diff --git a/web/modules/contrib/pathauto/src/PathautoState.php b/web/modules/contrib/pathauto/src/PathautoState.php new file mode 100644 index 000000000..6415252d8 --- /dev/null +++ b/web/modules/contrib/pathauto/src/PathautoState.php @@ -0,0 +1,97 @@ +value === NULL) { + // If no value has been set or loaded yet, try to load a value if this + // entity has already been saved. + $this->value = \Drupal::keyValue($this->getCollection()) + ->get($this->parent->getEntity()->id()); + // If it was not yet saved or no value was found, then set the flag to + // create the alias if there is a matching pattern. + if ($this->value === NULL) { + $entity = $this->parent->getEntity(); + $pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity); + $this->value = !empty($pattern) ? static::CREATE : static::SKIP; + } + } + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->value = $value; + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + + /** + * Returns TRUE if a value was set. + */ + public function hasValue() { + return $this->value !== NULL; + } + + /** + * Persists the state. + */ + public function persist() { + \Drupal::keyValue($this->getCollection())->set( + $this->parent->getEntity() + ->id(), $this->value + ); + } + + /** + * Deletes the stored state. + */ + public function purge() { + \Drupal::keyValue($this->getCollection()) + ->delete($this->parent->getEntity()->id()); + } + + /** + * Returns the key value collection that should be used for the given entity. + * @return string + */ + protected function getCollection() { + return 'pathauto_state.' . $this->parent->getEntity()->getEntityTypeId(); + } + +} diff --git a/web/modules/contrib/pathauto/src/PathautoWidget.php b/web/modules/contrib/pathauto/src/PathautoWidget.php new file mode 100644 index 000000000..3ca049008 --- /dev/null +++ b/web/modules/contrib/pathauto/src/PathautoWidget.php @@ -0,0 +1,71 @@ +getEntity(); + + // Taxonomy terms do not have an actual fieldset for path settings. + // Merge in the defaults. + // @todo Impossible to do this in widget, use another solution + /* + $form['path'] += array( + '#type' => 'fieldset', + '#title' => $this->t('URL path settings'), + '#collapsible' => TRUE, + '#collapsed' => empty($form['path']['alias']), + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('path-form'), + ), + '#access' => \Drupal::currentUser()->hasPermission('create url aliases') || \Drupal::currentUser()->hasPermission('administer url aliases'), + '#weight' => 30, + '#tree' => TRUE, + '#element_validate' => array('path_form_element_validate'), + );*/ + + $pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity); + if (empty($pattern)) { + return $element; + } + + if (\Drupal::currentUser()->hasPermission('administer pathauto')) { + $description = $this->t('Uncheck this to create a custom alias below. Configure URL alias patterns.', ['@admin_link' => Url::fromRoute('entity.pathauto_pattern.collection')->toString()]); + } + else { + $description = $this->t('Uncheck this to create a custom alias below.'); + } + + $element['pathauto'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Generate automatic URL alias'), + '#default_value' => $entity->path->pathauto, + '#description' => $description, + '#weight' => -1, + ); + + // Add JavaScript that will disable the path textfield when the automatic + // alias checkbox is checked. + $element['alias']['#states']['disabled']['input[name="path[' . $delta . '][pathauto]"]'] = array('checked' => TRUE); + + // Override path.module's vertical tabs summary. + $element['alias']['#attached']['library'] = ['pathauto/widget']; + + return $element; + } + +} diff --git a/web/modules/contrib/pathauto/src/Plugin/Action/UpdateAction.php b/web/modules/contrib/pathauto/src/Plugin/Action/UpdateAction.php new file mode 100644 index 000000000..bfe0f48b1 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Plugin/Action/UpdateAction.php @@ -0,0 +1,36 @@ +path->pathauto = PathautoState::CREATE; + \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'bulkupdate', array('message' => TRUE)); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'create url aliases'); + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php b/web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php new file mode 100644 index 000000000..cf7b4a2fa --- /dev/null +++ b/web/modules/contrib/pathauto/src/Plugin/Deriver/EntityAliasTypeDeriver.php @@ -0,0 +1,94 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->stringTranslation = $string_translation; + $this->tokenEntityMapper = $token_entity_mapper; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('string_translation'), + $container->get('token.entity_mapper') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + // An entity type must have a canonical link template and support fields. + if ($entity_type->hasLinkTemplate('canonical') && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class)) { + $base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); + if (!isset($base_fields['path'])) { + // The entity type does not have a path field and is therefore not + // supported. + continue; + } + $this->derivatives[$entity_type_id] = $base_plugin_definition; + $this->derivatives[$entity_type_id]['label'] = $entity_type->getLabel(); + $this->derivatives[$entity_type_id]['types'] = [$this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id)]; + $this->derivatives[$entity_type_id]['provider'] = $entity_type->getProvider(); + $this->derivatives[$entity_type_id]['context'] = [ + $entity_type_id => new ContextDefinition("entity:$entity_type_id", $this->t('@label being aliased', ['@label' => $entity_type->getLabel()])) + ]; + } + } + return $this->derivatives; + } + +} diff --git a/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/Broken.php b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/Broken.php new file mode 100644 index 000000000..cc48771a3 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/Broken.php @@ -0,0 +1,24 @@ +t('Broken type'); + } + +} diff --git a/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php new file mode 100644 index 000000000..121f04b38 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php @@ -0,0 +1,342 @@ +moduleHandler = $module_handler; + $this->languageManager = $language_manager; + $this->entityTypeManager = $entity_type_manager; + $this->keyValue = $key_value; + $this->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('module_handler'), + $container->get('language_manager'), + $container->get('entity_type.manager'), + $container->get('keyvalue'), + $container->get('database') + ); + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + $definition = $this->getPluginDefinition(); + // Cast the admin label to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + return (string) $definition['label']; + } + + /** + * {@inheritdoc} + */ + public function getTokenTypes() { + $definition = $this->getPluginDefinition(); + return $definition['types']; + } + + /** + * {@inheritdoc} + */ + public function batchUpdate($action, &$context) { + if (!isset($context['sandbox']['current'])) { + $context['sandbox']['count'] = 0; + $context['sandbox']['current'] = 0; + } + + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId()); + $id_key = $entity_type->getKey('id'); + + $query = $this->database->select($entity_type->get('base_table'), 'base_table'); + $query->leftJoin('url_alias', 'ua', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = ua.source"); + $query->addField('base_table', $id_key, 'id'); + + switch ($action) { + case 'create': + $query->isNull('ua.source'); + break; + case 'update': + $query->isNotNull('ua.source'); + break; + case 'all': + // Nothing to do. We want all paths. + break; + default: + // Unknown action. Abort! + return; + } + $query->condition('base_table.' . $id_key, $context['sandbox']['current'], '>'); + $query->orderBy('base_table.' . $id_key); + $query->addTag('pathauto_bulk_update'); + $query->addMetaData('entity', $this->getEntityTypeId()); + + // Get the total amount of items to process. + if (!isset($context['sandbox']['total'])) { + $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); + + // If there are no entities to update, then stop immediately. + if (!$context['sandbox']['total']) { + $context['finished'] = 1; + return; + } + } + + $query->range(0, 25); + $ids = $query->execute()->fetchCol(); + + $updates = $this->bulkUpdate($ids); + $context['sandbox']['count'] += count($ids); + $context['sandbox']['current'] = max($ids); + $context['results']['updates'] += $updates; + $context['message'] = $this->t('Updated alias for %label @id.', array('%label' => $entity_type->getLabel(), '@id' => end($ids))); + + if ($context['sandbox']['count'] != $context['sandbox']['total']) { + $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; + } + } + + /** + * {@inheritdoc} + */ + public function batchDelete(&$context) { + if (!isset($context['sandbox']['current'])) { + $context['sandbox']['count'] = 0; + $context['sandbox']['current'] = 0; + } + + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId()); + $id_key = $entity_type->getKey('id'); + + $query = $this->database->select($entity_type->get('base_table'), 'base_table'); + $query->innerJoin('url_alias', 'ua', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = ua.source"); + $query->addField('base_table', $id_key, 'id'); + $query->addField('ua', 'pid'); + $query->condition('ua.pid', $context['sandbox']['current'], '>'); + $query->orderBy('ua.pid'); + $query->addTag('pathauto_bulk_delete'); + $query->addMetaData('entity', $this->getEntityTypeId()); + + // Get the total amount of items to process. + if (!isset($context['sandbox']['total'])) { + $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); + + // If there are no entities to delete, then stop immediately. + if (!$context['sandbox']['total']) { + $context['finished'] = 1; + return; + } + } + + $query->range(0, 100); + $pids_by_id = $query->execute()->fetchAllKeyed(); + + $this->bulkDelete($pids_by_id); + $context['sandbox']['count'] += count($pids_by_id); + $context['sandbox']['current'] = max($pids_by_id); + $context['results']['deletions'][] = $this->getLabel(); + + if ($context['sandbox']['count'] != $context['sandbox']['total']) { + $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; + } + } + + /** + * Returns the entity type ID. + * + * @return string + * The entity type ID. + */ + protected function getEntityTypeId() { + return $this->getDerivativeId(); + } + + /** + * Update the URL aliases for multiple entities. + * + * @param array $ids + * An array of entity IDs. + * @param array $options + * An optional array of additional options. + * + * @return int + * The number of updated URL aliases. + */ + protected function bulkUpdate(array $ids, array $options = array()) { + $options += array('message' => FALSE); + $updates = 0; + + $entities = $this->entityTypeManager->getStorage($this->getEntityTypeId())->loadMultiple($ids); + foreach ($entities as $entity) { + // Update aliases for the entity's default language and its translations. + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + $translated_entity = $entity->getTranslation($langcode); + $result = \Drupal::service('pathauto.generator')->updateEntityAlias($translated_entity, 'bulkupdate', $options); + if ($result) { + $updates++; + } + } + } + + if (!empty($options['message'])) { + drupal_set_message(\Drupal::translation()->formatPlural(count($ids), 'Updated 1 %label URL alias.', 'Updated @count %label URL aliases.'), array('%label' => $this->getLabel())); + } + + return $updates; + } + + /** + * Deletes the URL aliases for multiple entities. + * + * @param int[] $pids_by_id + * A list of path IDs keyed by entity ID. + */ + protected function bulkDelete(array $pids_by_id) { + $collection = 'pathauto_state.' . $this->getEntityTypeId(); + $states = $this->keyValue->get($collection)->getMultiple(array_keys($pids_by_id)); + + $pids = []; + foreach ($pids_by_id as $id => $pid) { + // Only delete aliases that were created by this module. + if (isset($states[$id]) && $states[$id] == PathautoState::CREATE) { + $pids[] = $pid; + } + } + \Drupal::service('pathauto.alias_storage_helper')->deleteMultiple($pids); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = []; + $dependencies['module'][] = $this->entityTypeManager->getDefinition($this->getEntityTypeId())->getProvider(); + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function applies($object) { + return $object instanceof FieldableEntityInterface && $object->getEntityTypeId() == $this->getEntityTypeId(); + } + + /** + * {@inheritdoc} + */ + public function getSourcePrefix() { + if (empty($this->prefix)) { + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId()); + $path = $entity_type->getLinkTemplate('canonical'); + $this->prefix = substr($path, 0, strpos($path, '{')); + } + return $this->prefix; + } + + /** + * {@inheritdoc} + */ + public function setContextValue($name, $value) { + // Overridden to avoid merging existing cacheability metadata, which is not + // relevant for alias type plugins. + $this->context[$name] = new Context($this->getContextDefinition($name), $value); + return $this; + } + + +} diff --git a/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php new file mode 100644 index 000000000..a572593ea --- /dev/null +++ b/web/modules/contrib/pathauto/src/Plugin/pathauto/AliasType/ForumAliasType.php @@ -0,0 +1,106 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('module_handler'), + $container->get('language_manager'), + $container->get('entity_type.manager'), + $container->get('keyvalue'), + $container->get('database'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + protected function getEntityTypeId() { + return 'taxonomy_term'; + } + + /** + * {@inheritdoc} + */ + public function getSourcePrefix() { + return '/forum/'; + } + + /** + * {@inheritdoc} + */ + public function applies($object) { + if (parent::applies($object)) { + /** @var \Drupal\taxonomy\TermInterface $object */ + $vid = $this->configFactory->get('forum.settings')->get('vocabulary'); + return $object->getVocabularyId() == $vid; + } + return FALSE; + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php new file mode 100644 index 000000000..2fa2194ab --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoBulkUpdateTest.php @@ -0,0 +1,156 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + + $this->patterns = array(); + $this->patterns['node'] = $this->createPattern('node', '/content/[node:title]'); + $this->patterns['user'] = $this->createPattern('user', '/users/[user:name]'); + $this->patterns['forum'] = $this->createPattern('forum', '/forums/[term:name]'); + } + + function testBulkUpdate() { + // Create some nodes. + $this->nodes = array(); + for ($i = 1; $i <= 5; $i++) { + $node = $this->drupalCreateNode(); + $this->nodes[$node->id()] = $node; + } + + // Clear out all aliases. + $this->deleteAllAliases(); + + // Bulk create aliases. + $edit = array( + 'update[canonical_entities:node]' => TRUE, + 'update[canonical_entities:user]' => TRUE, + 'update[forum]' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + + // This has generated 8 aliases: 5 nodes, 2 users and 1 forum. + $this->assertText('Generated 8 URL aliases.'); + + // Check that aliases have actually been created. + foreach ($this->nodes as $node) { + $this->assertEntityAliasExists($node); + } + $this->assertEntityAliasExists($this->adminUser); + // This is the default "General discussion" forum. + $this->assertAliasExists(['source' => '/taxonomy/term/1']); + + // Add a new node. + $new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => PathautoState::SKIP))); + + // Run the update again which should not run against any nodes. + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + $this->assertText('No new URL aliases to generate.'); + $this->assertNoEntityAliasExists($new_node); + + // Make sure existing aliases can be overriden. + $this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_DELETE], t('Save configuration')); + + // Patterns did not change, so no aliases should be regenerated. + $edit['action'] = 'all'; + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + $this->assertText('No new URL aliases to generate.'); + + // Update the node pattern, and leave other patterns alone. Existing nodes should get a new alias, + // except the node above whose alias is manually set. Other aliases must be left alone. + $this->patterns['node']->delete(); + $this->patterns['node'] = $this->createPattern('node', '/archive/node-[node:nid]'); + + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + $this->assertText('Generated 5 URL aliases.'); + + // Prevent existing aliases to be overriden. The bulk generate page should only offer + // to create an alias for paths which have none. + $this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW], t('Save configuration')); + + $this->drupalGet('admin/config/search/path/update_bulk'); + $this->assertFieldByName('action', 'create'); + $this->assertText('Pathauto settings are set to ignore paths which already have a URL alias.'); + $this->assertNoFieldByName('action', 'update'); + $this->assertNoFieldByName('action', 'all'); + } + + /** + * Tests alias generation for nodes that existed before installing Pathauto. + */ + function testBulkUpdateExistingContent() { + // Create a node. + $node = $this->drupalCreateNode(); + + // Delete its alias and Pathauto metadata. + \Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($node); + $node->path->first()->get('pathauto')->purge(); + \Drupal::entityTypeManager()->getStorage('node')->resetCache(array($node->id())); + + // Execute bulk generation. + // Bulk create aliases. + $edit = array( + 'update[canonical_entities:node]' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + + // Verify that the alias was created for the node. + $this->assertText('Generated 1 URL alias.'); + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php new file mode 100644 index 000000000..c8cc1fff9 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoEnablingEntityTypesTest.php @@ -0,0 +1,87 @@ +drupalCreateContentType(array('type' => 'article')); + $this->addDefaultCommentField('node', 'article'); + + $permissions = array( + 'administer pathauto', + 'administer url aliases', + 'create url aliases', + 'administer nodes', + 'post comments', + ); + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * A suite of tests to verify if the feature to enable and disable the + * ability to define alias patterns for a given entity type works. Test with + * the comment module, as it is not enabled by default. + */ + function testEnablingEntityTypes() { + // Verify that the comment entity type is not available when trying to add + // a new pattern, nor "broken". + $this->drupalGet('/admin/config/search/path/patterns/add'); + $this->assertEqual(count($this->cssSelect('option[value = "canonical_entities:comment"]:contains(Comment)')), 0); + $this->assertEqual(count($this->cssSelect('option:contains(Broken)')), 0); + + // Enable the entity type and create a pattern for it. + $this->drupalGet('/admin/config/search/path/settings'); + $edit = [ + 'enabled_entity_types[comment]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Save configuration'); + $this->createPattern('comment', '/comment/[comment:body]'); + + // Create a node, a comment type and a comment entity. + $node = $this->drupalCreateNode(['type' => 'article']); + $this->drupalGet('/node/' . $node->id()); + $edit = [ + 'comment_body[0][value]' => 'test-body', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + + // Verify that an alias has been generated and that the type can no longer + // be disabled. + $this->assertAliasExists(['alias' => '/comment/test-body']); + $this->drupalGet('/admin/config/search/path/settings'); + $this->assertEqual(count($this->cssSelect('input[name = "enabled_entity_types[comment]"][disabled = "disabled"]')), 1); + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoLocaleTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoLocaleTest.php new file mode 100644 index 000000000..dd271ef59 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoLocaleTest.php @@ -0,0 +1,201 @@ +drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } + + /** + * Test that when an English node is updated, its old English alias is + * updated and its newer French alias is left intact. + */ + function testLanguageAliases() { + + $this->createPattern('node', '/content/[node:title]'); + + // Add predefined French language. + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $node = array( + 'title' => 'English node', + 'langcode' => 'en', + 'path' => array(array( + 'alias' => '/english-node', + 'pathauto' => FALSE, + )), + ); + $node = $this->drupalCreateNode($node); + $english_alias = \Drupal::service('path.alias_storage')->load(array('alias' => '/english-node', 'langcode' => 'en')); + $this->assertTrue($english_alias, 'Alias created with proper language.'); + + // Also save a French alias that should not be left alone, even though + // it is the newer alias. + $this->saveEntityAlias($node, '/french-node', 'fr'); + + // Add an alias with the soon-to-be generated alias, causing the upcoming + // alias update to generate a unique alias with the '-0' suffix. + $this->saveAlias('/node/invalid', '/content/english-node', Language::LANGCODE_NOT_SPECIFIED); + + // Update the node, triggering a change in the English alias. + $node->path->pathauto = PathautoState::CREATE; + $node->save(); + + // Check that the new English alias replaced the old one. + $this->assertEntityAlias($node, '/content/english-node-0', 'en'); + $this->assertEntityAlias($node, '/french-node', 'fr'); + $this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => '/content/english-node-0')); + + // Create a new node with the same title as before but without + // specifying a language. + $node = $this->drupalCreateNode(array('title' => 'English node', 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED)); + + // Check that the new node had a unique alias generated with the '-0' + // suffix. + $this->assertEntityAlias($node, '/content/english-node-0', LanguageInterface::LANGCODE_NOT_SPECIFIED); + } + + /** + * Test that patterns work on multilingual content. + */ + function testLanguagePatterns() { + $this->drupalLogin($this->rootUser); + + // Add French language. + $edit = array( + 'predefined_langcode' => 'fr', + ); + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + + $this->enableArticleTranslation(); + + // Create a pattern for English articles. + $this->drupalGet('admin/config/search/path/patterns/add'); + $edit = array( + 'type' => 'canonical_entities:node', + ); + $this->drupalPostAjaxForm(NULL, $edit, 'type'); + $edit += array( + 'pattern' => '/the-articles/[node:title]', + 'label' => 'English articles', + 'id' => 'english_articles', + 'bundles[article]' => TRUE, + 'languages[en]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Pattern English articles saved.'); + + // Create a pattern for French articles. + $this->drupalGet('admin/config/search/path/patterns/add'); + $edit = array( + 'type' => 'canonical_entities:node', + ); + $this->drupalPostAjaxForm(NULL, $edit, 'type'); + $edit += array( + 'pattern' => '/les-articles/[node:title]', + 'label' => 'French articles', + 'id' => 'french_articles', + 'bundles[article]' => TRUE, + 'languages[fr]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Pattern French articles saved.'); + + // Create a node and its translation. Assert aliases. + $edit = array( + 'title[0][value]' => 'English node', + 'langcode[0][value]' => 'en', + ); + $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $english_node = $this->drupalGetNodeByTitle('English node'); + $this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en'); + + $this->drupalGet('node/' . $english_node->id() . '/translations'); + $this->clickLink(t('Add')); + $edit = array( + 'title[0][value]' => 'French node', + ); + $this->drupalPostForm(NULL, $edit, t('Save and keep published (this translation)')); + $this->rebuildContainer(); + $english_node = $this->drupalGetNodeByTitle('English node'); + $french_node = $english_node->getTranslation('fr'); + $this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr'); + + // Bulk delete and Bulk generate patterns. Assert aliases. + $this->deleteAllAliases(); + // Bulk create aliases. + $edit = array( + 'update[canonical_entities:node]' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + $this->assertText(t('Generated 2 URL aliases.')); + $this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en'); + $this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr'); + } + + /** + * Tests the alias created for a node with language Not Applicable. + */ + public function testLanguageNotApplicable() { + $this->drupalLogin($this->rootUser); + $this->enableArticleTranslation(); + + // Create a pattern for nodes. + $pattern = $this->createPattern('node', '/content/[node:title]', -1); + $pattern->save(); + + // Create a node with language Not Applicable. + $node = $this->createNode(['type' => 'article', 'title' => 'Test node', 'langcode' => LanguageInterface::LANGCODE_NOT_APPLICABLE]); + + // Check that the generated alias has language Not Specified. + $alias = \Drupal::service('pathauto.alias_storage_helper')->loadBySource('/node/' . $node->id()); + $this->assertEqual($alias['langcode'], LanguageInterface::LANGCODE_NOT_SPECIFIED, 'PathautoGenerator::createEntityAlias() adjusts the alias langcode from Not Applicable to Not Specified.'); + + // Check that the alias works. + $this->drupalGet('content/test-node'); + $this->assertResponse(200); + } + + /** + * Enables content translation on articles. + */ + protected function enableArticleTranslation() { + // Enable content translation on articles. + $this->drupalGet('admin/config/regional/content-language'); + $edit = array( + 'entity_types[node]' => TRUE, + 'settings[node][article][translatable]' => TRUE, + 'settings[node][article][settings][language][language_alterable]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, t('Save configuration')); + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php new file mode 100644 index 000000000..93f4ca32d --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoMassDeleteTest.php @@ -0,0 +1,200 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + + $this->createPattern('node', '/content/[node:title]'); + $this->createPattern('user', '/users/[user:name]'); + $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]'); + } + + /** + * Tests the deletion of all the aliases. + */ + function testDeleteAll() { + // 1. Test that deleting all the aliases, of any type, works. + $this->generateAliases(); + $edit = array( + 'delete[all_aliases]' => TRUE, + 'options[keep_custom_aliases]' => FALSE, + ); + $this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!')); + $this->assertText(t('All of your path aliases have been deleted.')); + $this->assertUrl('admin/config/search/path/delete_bulk'); + + // Make sure that all of them are actually deleted. + $aliases = \Drupal::database()->select('url_alias', 'ua')->fields('ua', array())->execute()->fetchAll(); + $this->assertEqual($aliases, array(), "All the aliases have been deleted."); + + // 2. Test deleting only specific (entity type) aliases. + $manager = $this->container->get('plugin.manager.alias_type'); + $pathauto_plugins = array('canonical_entities:node' => 'nodes', 'canonical_entities:taxonomy_term' => 'terms', 'canonical_entities:user' => 'accounts'); + foreach ($pathauto_plugins as $pathauto_plugin => $attribute) { + $this->generateAliases(); + $edit = array( + 'delete[plugins][' . $pathauto_plugin . ']' => TRUE, + 'options[keep_custom_aliases]' => FALSE, + ); + $this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!')); + $alias_type = $manager->createInstance($pathauto_plugin); + $this->assertRaw(t('All of your %label path aliases have been deleted.', array('%label' => $alias_type->getLabel()))); + // Check that the aliases were actually deleted. + foreach ($this->{$attribute} as $entity) { + $this->assertNoEntityAlias($entity); + } + + // Check that the other aliases are not deleted. + foreach ($pathauto_plugins as $_pathauto_plugin => $_attribute) { + // Skip the aliases that should be deleted. + if ($_pathauto_plugin == $pathauto_plugin) { + continue; + } + foreach ($this->{$_attribute} as $entity) { + $this->assertEntityAliasExists($entity); + } + } + } + + // 3. Test deleting automatically generated aliases only. + $this->generateAliases(); + $edit = array( + 'delete[all_aliases]' => TRUE, + 'options[keep_custom_aliases]' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!')); + $this->assertText(t('All of your automatically generated path aliases have been deleted.')); + $this->assertUrl('admin/config/search/path/delete_bulk'); + + // Make sure that only custom aliases and aliases with no information about + // their state still exist. + $aliases = \Drupal::database()->select('url_alias', 'ua')->fields('ua', ['source'])->execute()->fetchCol(); + $this->assertEqual($aliases, ['/node/101', '/node/104', '/node/105'], 'Custom aliases still exist.'); + } + + /** + * Helper function to generate aliases. + */ + function generateAliases() { + // Delete all aliases to avoid duplicated aliases. They will be recreated below. + $this->deleteAllAliases(); + + // We generate a bunch of aliases for nodes, users and taxonomy terms. If + // the entities are already created we just update them, otherwise we create + // them. + if (empty($this->nodes)) { + // Create a large number of nodes (100+) to make sure that the batch code works. + for ($i = 1; $i <= 105; $i++) { + // Set the alias of two nodes manually. + $settings = ($i > 103) ? ['path' => ['alias' => "/custom_alias_$i", 'pathauto' => PathautoState::SKIP]] : []; + $node = $this->drupalCreateNode($settings); + $this->nodes[$node->id()] = $node; + } + } + else { + foreach ($this->nodes as $node) { + if ($node->id() > 103) { + // The alias is set manually. + $node->set('path', ['alias' => '/custom_alias_' . $node->id()]); + } + $node->save(); + } + } + // Delete information about the state of an alias to make sure that aliases + // with no such data are left alone by default. + \Drupal::keyValue('pathauto_state.node')->delete(101); + + if (empty($this->accounts)) { + for ($i = 1; $i <= 5; $i++) { + $account = $this->drupalCreateUser(); + $this->accounts[$account->id()] = $account; + } + } + else { + foreach ($this->accounts as $id => $account) { + $account->save(); + } + } + + if (empty($this->terms)) { + $vocabulary = $this->addVocabulary(array('name' => 'test vocabulary', 'vid' => 'test_vocabulary')); + for ($i = 1; $i <= 5; $i++) { + $term = $this->addTerm($vocabulary); + $this->terms[$term->id()] = $term; + } + } + else { + foreach ($this->terms as $term) { + $term->save(); + } + } + + // Check that we have aliases for the entities. + foreach (array('nodes', 'accounts', 'terms') as $attribute) { + foreach ($this->{$attribute} as $entity) { + $this->assertEntityAliasExists($entity); + } + } + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php new file mode 100644 index 000000000..95f5eb54c --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoNodeWebTest.php @@ -0,0 +1,292 @@ +drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article')); + + // Allow other modules to add additional permissions for the admin user. + $permissions = array( + 'administer pathauto', + 'administer url aliases', + 'create url aliases', + 'administer nodes', + 'bypass node access', + 'access content overview', + ); + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + + $this->createPattern('node', '/content/[node:title]'); + } + + /** + * Tests editing nodes with different settings. + */ + function testNodeEditing() { + // Ensure that the Pathauto checkbox is checked by default on the node add form. + $this->drupalGet('node/add/page'); + $this->assertFieldChecked('edit-path-0-pathauto'); + + // Create a node by saving the node form. + $title = ' Testing: node title ['; + $automatic_alias = '/content/testing-node-title'; + $this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save and publish')); + $node = $this->drupalGetNodeByTitle($title); + + // Look for alias generated in the form. + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.'); + + // Check whether the alias actually works. + $this->drupalGet($automatic_alias); + $this->assertText($title, 'Node accessible through automatic alias.'); + + // Manually set the node's alias. + $manual_alias = '/content/' . $node->id(); + $edit = array( + 'path[0][pathauto]' => FALSE, + 'path[0][alias]' => $manual_alias, + ); + $this->drupalPostForm($node->toUrl('edit-form'), $edit, t('Save and keep published')); + $this->assertText(t('@type @title has been updated.', array('@type' => 'page', '@title' => $title))); + + // Check that the automatic alias checkbox is now unchecked by default. + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertNoFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $manual_alias); + + // Submit the node form with the default values. + $this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save and keep published')); + $this->assertText(t('@type @title has been updated.', array('@type' => 'page', '@title' => $title))); + + // Test that the old (automatic) alias has been deleted and only accessible + // through the new (manual) alias. + $this->drupalGet($automatic_alias); + $this->assertResponse(404, 'Node not accessible through automatic alias.'); + $this->drupalGet($manual_alias); + $this->assertText($title, 'Node accessible through manual alias.'); + + // Test that the manual alias is not kept for new nodes when the pathauto + // checkbox is ticked. + $title = 'Automatic Title'; + $edit = array( + 'title[0][value]' => $title, + 'path[0][pathauto]' => TRUE, + 'path[0][alias]' => '/should-not-get-created', + ); + $this->drupalPostForm('node/add/page', $edit, t('Save and publish')); + $this->assertNoAliasExists(array('alias' => 'should-not-get-created')); + $node = $this->drupalGetNodeByTitle($title); + $this->assertEntityAlias($node, '/content/automatic-title'); + + // Remove the pattern for nodes, the pathauto checkbox should not be + // displayed. + $ids = \Drupal::entityQuery('pathauto_pattern') + ->condition('type', 'canonical_entities:node') + ->execute(); + foreach (PathautoPattern::loadMultiple($ids) as $pattern) { + $pattern->delete(); + } + + $this->drupalGet('node/add/article'); + $this->assertNoFieldById('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', ''); + + $edit = array(); + $edit['title'] = 'My test article'; + $this->drupalCreateNode($edit); + //$this->drupalPostForm(NULL, $edit, t('Save and keep published')); + $node = $this->drupalGetNodeByTitle($edit['title']); + + // Pathauto checkbox should still not exist. + $this->drupalGet($node->toUrl('edit-form')); + $this->assertNoFieldById('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', ''); + $this->assertNoEntityAlias($node); + } + + /** + * Test node operations. + */ + function testNodeOperations() { + $node1 = $this->drupalCreateNode(array('title' => 'node1')); + $node2 = $this->drupalCreateNode(array('title' => 'node2')); + + // Delete all current URL aliases. + $this->deleteAllAliases(); + + $this->drupalGet('admin/content'); + + // Check which of the two nodes is first. + if (strpos($this->getTextContent(), 'node1') < strpos($this->getTextContent(), 'node2')) { + $index = 0; + } + else { + $index = 1; + } + + $edit = array( + 'action' => 'pathauto_update_alias_node', + 'node_bulk_form[' . $index . ']' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, t('Apply to selected items')); + $this->assertText('Update URL alias was applied to 1 item.'); + + $this->assertEntityAlias($node1, '/content/' . $node1->getTitle()); + $this->assertEntityAlias($node2, '/node/' . $node2->id()); + } + + /** + * @todo Merge this with existing node test methods? + */ + public function testNodeState() { + $nodeNoAliasUser = $this->drupalCreateUser(array('bypass node access')); + $nodeAliasUser = $this->drupalCreateUser(array('bypass node access', 'create url aliases')); + + $node = $this->drupalCreateNode(array( + 'title' => 'Node version one', + 'type' => 'page', + 'path' => array( + 'pathauto' => PathautoState::SKIP, + ), + )); + + $this->assertNoEntityAlias($node); + + // Set a manual path alias for the node. + $node->path->alias = '/test-alias'; + $node->save(); + + // Ensure that the pathauto field was saved to the database. + \Drupal::entityTypeManager()->getStorage('node')->resetCache(); + $node = Node::load($node->id()); + $this->assertIdentical($node->path->pathauto, PathautoState::SKIP); + + // Ensure that the manual path alias was saved and an automatic alias was not generated. + $this->assertEntityAlias($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + + // Save the node as a user who does not have access to path fieldset. + $this->drupalLogin($nodeNoAliasUser); + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertNoFieldByName('path[0][pathauto]'); + + $edit = array('title[0][value]' => 'Node version two'); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Basic page Node version two has been updated.'); + + $this->assertEntityAlias($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + $this->assertNoEntityAliasExists($node, '/content/node-version-two'); + + // Load the edit node page and check that the Pathauto checkbox is unchecked. + $this->drupalLogin($nodeAliasUser); + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertNoFieldChecked('edit-path-0-pathauto'); + + // Edit the manual alias and save the node. + $edit = array( + 'title[0][value]' => 'Node version three', + 'path[0][alias]' => '/manually-edited-alias', + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Basic page Node version three has been updated.'); + + $this->assertEntityAlias($node, '/manually-edited-alias'); + $this->assertNoEntityAliasExists($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + $this->assertNoEntityAliasExists($node, '/content/node-version-two'); + $this->assertNoEntityAliasExists($node, '/content/node-version-three'); + + // Programatically save the node with an automatic alias. + \Drupal::entityTypeManager()->getStorage('node')->resetCache(); + $node = Node::load($node->id()); + $node->path->pathauto = PathautoState::CREATE; + $node->save(); + + // Ensure that the pathauto field was saved to the database. + \Drupal::entityTypeManager()->getStorage('node')->resetCache(); + $node = Node::load($node->id()); + $this->assertIdentical($node->path->pathauto, PathautoState::CREATE); + + $this->assertEntityAlias($node, '/content/node-version-three'); + $this->assertNoEntityAliasExists($node, '/manually-edited-alias'); + $this->assertNoEntityAliasExists($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + $this->assertNoEntityAliasExists($node, '/content/node-version-two'); + + $node->delete(); + $this->assertNull(\Drupal::keyValue('pathauto_state.node')->get($node->id()), 'Pathauto state was deleted'); + } + + + /** + * Tests that nodes without a Pathauto pattern can set custom aliases. + */ + public function testCustomAliasWithoutPattern() { + // First, delete all patterns to be sure that there will be no match. + $entity_ids = \Drupal::entityQuery('pathauto_pattern')->execute(); + $entities = PathautoPattern::loadMultiple($entity_ids); + foreach ($entities as $entity) { + $entity->delete(); + } + + // Next, create a node with a custom alias. + $edit = [ + 'title[0][value]' => 'Sample article', + 'path[0][alias]' => '/sample-article', + ]; + $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $this->assertText(t('article Sample article has been created.')); + + // Test the alias. + $this->assertAliasExists(array('alias' => '/sample-article')); + $this->drupalGet('sample-article'); + $this->assertResponse(200, 'A node without a pattern can have a custom alias.'); + + // Now create a node through the API. + $node = Node::create(['type' => 'article', 'title' => 'Sample article API', 'path' => ['alias' => '/sample-article-api']]); + $node->save(); + + // Test the alias. + $this->assertAliasExists(['alias' => '/sample-article-api']); + $this->drupalGet('sample-article-api'); + $this->assertResponse(200); + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php new file mode 100644 index 000000000..9a4a69092 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoSettingsFormWebTest.php @@ -0,0 +1,240 @@ + FALSE, + 'separator' => '-', + 'case' => '1', + 'max_length' => '100', + 'max_component_length' => '100', + 'update_action' => '2', + 'transliterate' => '1', + 'reduce_ascii' => FALSE, + 'ignore_words' => 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with', + ); + + /** + * Punctuation form items with default values. + * + * @var array + */ + protected $defaultPunctuations = array( + 'punctuation[double_quotes]' => '0', + 'punctuation[quotes]' => '0', + 'punctuation[backtick]' => '0', + 'punctuation[comma]' => '0', + 'punctuation[period]' => '0', + 'punctuation[hyphen]' => '1', + 'punctuation[underscore]' => '0', + 'punctuation[colon]' => '0', + 'punctuation[semicolon]' => '0', + 'punctuation[pipe]' => '0', + 'punctuation[left_curly]' => '0', + 'punctuation[left_square]' => '0', + 'punctuation[right_curly]' => '0', + 'punctuation[right_square]' => '0', + 'punctuation[plus]' => '0', + 'punctuation[equal]' => '0', + 'punctuation[asterisk]' => '0', + 'punctuation[ampersand]' => '0', + 'punctuation[percent]' => '0', + 'punctuation[caret]' => '0', + 'punctuation[dollar]' => '0', + 'punctuation[hash]' => '0', + 'punctuation[exclamation]' => '0', + 'punctuation[tilde]' => '0', + 'punctuation[left_parenthesis]' => '0', + 'punctuation[right_parenthesis]' => '0', + 'punctuation[question_mark]' => '0', + 'punctuation[less_than]' => '0', + 'punctuation[greater_than]' => '0', + 'punctuation[slash]' => '0', + 'punctuation[back_slash]' => '0', + ); + + /** + * {inheritdoc} + */ + function setUp() { + parent::setUp(); + + $this->drupalCreateContentType(array('type' => 'article')); + + $permissions = array( + 'administer pathauto', + 'notify of path changes', + 'administer url aliases', + 'create url aliases', + 'administer nodes', + 'bypass node access', + ); + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + $this->createPattern('node', '/content/[node:title]'); + } + + /** + * Test if the default values are shown correctly in the form. + */ + function testDefaultFormValues() { + $this->drupalGet('/admin/config/search/path/settings'); + $this->assertNoFieldChecked('edit-verbose'); + $this->assertField('edit-separator', $this->defaultFormValues['separator']); + $this->assertFieldChecked('edit-case'); + $this->assertField('edit-max-length', $this->defaultFormValues['max_length']); + $this->assertField('edit-max-component-length', $this->defaultFormValues['max_component_length']); + $this->assertFieldChecked('edit-update-action-2'); + $this->assertFieldChecked('edit-transliterate'); + $this->assertNoFieldChecked('edit-reduce-ascii'); + $this->assertField('edit-ignore-words', $this->defaultFormValues['ignore_words']); + } + + /** + * Test the verbose option. + */ + function testVerboseOption() { + $edit = array('verbose' => '1'); + $this->drupalPostForm('/admin/config/search/path/settings', $edit, t('Save configuration')); + $this->assertText(t('The configuration options have been saved.')); + $this->assertFieldChecked('edit-verbose'); + + $title = 'Verbose settings test'; + $this->drupalGet('/node/add/article'); + $this->assertFieldChecked('edit-path-0-pathauto'); + $this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save and publish')); + $this->assertText('Created new alias /content/verbose-settings-test for'); + + $node = $this->drupalGetNodeByTitle($title); + $this->drupalPostForm('/node/' . $node->id() . '/edit', array('title[0][value]' => 'Updated title'), t('Save and keep published')); + $this->assertText('Created new alias /content/updated-title for'); + $this->assertText('replacing /content/verbose-settings-test.'); + } + + /** + * Tests generating aliases with different settings. + */ + function testSettingsForm() { + // Ensure the separator settings apply correctly. + $this->checkAlias('My awesome content', '/content/my.awesome.content', array('separator' => '.')); + + // Ensure the character case setting works correctly. + // Leave case the same as source token values. + $this->checkAlias('My awesome Content', '/content/My-awesome-Content', array('case' => FALSE)); + $this->checkAlias('Change Lower', '/content/change-lower', array('case' => '1')); + + // Ensure the maximum alias length is working. + $this->checkAlias('My awesome Content', '/content/my-awesome', array('max_length' => '23')); + + // Ensure the maximum component length is working. + $this->checkAlias('My awesome Content', '/content/my', array('max_component_length' => '2')); + + // Ensure transliteration option is working. + $this->checkAlias('è é àl ö äl ü', '/content/e-e-al-o-al-u', array('transliterate' => '1')); + $this->checkAlias('è é àl äl ö ü', '/content/è-é-àl-äl-ö-ü', array('transliterate' => FALSE)); + + $ignore_words = 'a, new, very, should'; + $this->checkAlias('a very new alias to test', '/content/alias-to-test', array('ignore_words' => $ignore_words)); + } + + /** + * Test the punctuation setting form items. + */ + function testPunctuationSettings() { + // Test the replacement of punctuations. + $settings = []; + foreach ($this->defaultPunctuations as $key => $punctuation) { + $settings[$key] = PathautoGeneratorInterface::PUNCTUATION_REPLACE; + } + + $title = 'aa"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3'; + $alias = '/content/aa-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-1-2-3'; + $this->checkAlias($title, $alias, $settings); + + // Test the removal of punctuations. + $settings = []; + foreach ($this->defaultPunctuations as $key => $punctuation) { + $settings[$key] = PathautoGeneratorInterface::PUNCTUATION_REMOVE; + } + + $title = 'a"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3'; + $alias = '/content/abcdefghijklmnopqrstuvwxyz123'; + $this->checkAlias($title, $alias, $settings); + + // Keep all punctuations in alias. + $settings = []; + foreach ($this->defaultPunctuations as $key => $punctuation) { + $settings[$key] = PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING; + } + + $title = 'al"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3'; + $alias = '/content/al"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3'; + $this->checkAlias($title, $alias, $settings); + } + + /** + * Helper method to check the an aliases. + * + * @param string $title + * The node title to build the aliases from. + * @param string $alias + * The expected alias. + * @param array $settings + * The form values the alias should be generated with. + */ + protected function checkAlias($title, $alias, $settings = array()) { + // Submit the settings form. + $edit = array_merge($this->defaultFormValues + $this->defaultPunctuations, $settings); + $this->drupalPostForm('/admin/config/search/path/settings', $edit, t('Save configuration')); + $this->assertText(t('The configuration options have been saved.')); + + // If we do not clear the caches here, AliasCleaner will use its + // cleanStringCache instance variable. Due to that the creation of aliases + // with $this->createNode() will only work correctly on the first call. + \Drupal::service('pathauto.generator')->resetCaches(); + + // Create a node and check if the settings applied. + $node = $this->createNode( + array( + 'title' => $title, + 'type' => 'article', + ) + ); + + $this->drupalGet($alias); + $this->assertResponse(200); + $this->assertEntityAlias($node, $alias); + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php new file mode 100644 index 000000000..e1a7e88a1 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoTaxonomyWebTest.php @@ -0,0 +1,102 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + + $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]'); + } + + + /** + * Basic functional testing of Pathauto with taxonomy terms. + */ + function testTermEditing() { + $this->drupalGet('admin/structure'); + $this->drupalGet('admin/structure/taxonomy'); + + // Add vocabulary "tags". + $vocabulary = $this->addVocabulary(array('name' => 'tags', 'vid' => 'tags')); + + // Create term for testing. + $name = 'Testing: term name ['; + $automatic_alias = '/tags/testing-term-name'; + $this->drupalPostForm('admin/structure/taxonomy/manage/tags/add', array('name[0][value]' => $name), 'Save'); + $name = trim($name); + $this->assertText("Created new term $name."); + $term = $this->drupalGetTermByName($name); + + // Look for alias generated in the form. + $this->drupalGet("taxonomy/term/{$term->id()}/edit"); + $this->assertFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.'); + + // Check whether the alias actually works. + $this->drupalGet($automatic_alias); + $this->assertText($name, 'Term accessible through automatic alias.'); + + // Manually set the term's alias. + $manual_alias = '/tags/' . $term->id(); + $edit = array( + 'path[0][pathauto]' => FALSE, + 'path[0][alias]' => $manual_alias, + ); + $this->drupalPostForm("taxonomy/term/{$term->id()}/edit", $edit, t('Save')); + $this->assertText("Updated term $name."); + + // Check that the automatic alias checkbox is now unchecked by default. + $this->drupalGet("taxonomy/term/{$term->id()}/edit"); + $this->assertNoFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $manual_alias); + + // Submit the term form with the default values. + $this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save')); + $this->assertText("Updated term $name."); + + // Test that the old (automatic) alias has been deleted and only accessible + // through the new (manual) alias. + $this->drupalGet($automatic_alias); + $this->assertResponse(404, 'Term not accessible through automatic alias.'); + $this->drupalGet($manual_alias); + $this->assertText($name, 'Term accessible through manual alias.'); + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php b/web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php new file mode 100644 index 000000000..5fc6b4070 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoTestHelperTrait.php @@ -0,0 +1,190 @@ + Unicode::strtolower($this->randomMachineName()), + 'type' => $type, + 'pattern' => $pattern, + 'weight' => $weight, + ]); + $pattern->save(); + return $pattern; + } + + /** + * Add a bundle condition to a pathauto pattern. + * + * @param \Drupal\pathauto\PathautoPatternInterface $pattern + * The pattern. + * @param string $entity_type + * The entity type ID. + * @param string $bundle + * The bundle + */ + protected function addBundleCondition(PathautoPatternInterface $pattern, $entity_type, $bundle) { + $plugin_id = $entity_type == 'node' ? 'node_type' : 'entity_bundle:' . $entity_type; + + $pattern->addSelectionCondition( + [ + 'id' => $plugin_id, + 'bundles' => [ + $bundle => $bundle, + ], + 'negate' => FALSE, + 'context_mapping' => [ + $entity_type => $entity_type, + ] + ] + ); + } + + public function assertToken($type, $object, $token, $expected) { + $bubbleable_metadata = new BubbleableMetadata(); + $tokens = \Drupal::token()->generate($type, array($token => $token), array($type => $object), [], $bubbleable_metadata); + $tokens += array($token => ''); + $this->assertIdentical($tokens[$token], $expected, t("Token value for [@type:@token] was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $tokens[$token], '@expected' => $expected))); + } + + public function saveAlias($source, $alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) { + \Drupal::service('path.alias_storage')->delete(array('source' => $source, 'language', 'langcode' => $langcode)); + return \Drupal::service('path.alias_storage')->save($source, $alias, $langcode); + } + + public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NULL) { + // By default, use the entity language. + if (!$langcode) { + $langcode = $entity->language()->getId(); + } + return $this->saveAlias('/' . $entity->toUrl()->getInternalPath(), $alias, $langcode); + } + + public function assertEntityAlias(EntityInterface $entity, $expected_alias, $langcode = NULL) { + // By default, use the entity language. + if (!$langcode) { + $langcode = $entity->language()->getId(); + } + $this->assertAlias('/' . $entity->toUrl()->getInternalPath(), $expected_alias, $langcode); + } + + public function assertEntityAliasExists(EntityInterface $entity) { + return $this->assertAliasExists(array('source' => '/' . $entity->toUrl()->getInternalPath())); + } + + public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) { + // By default, use the entity language. + if (!$langcode) { + $langcode = $entity->language()->getId(); + } + $this->assertEntityAlias($entity, '/' . $entity->toUrl()->getInternalPath(), $langcode); + } + + public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL) { + $path = array('source' => '/' . $entity->toUrl()->getInternalPath()); + if (!empty($alias)) { + $path['alias'] = $alias; + } + $this->assertNoAliasExists($path); + } + + public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) { + \Drupal::service('path.alias_manager')->cacheClear($source); + $this->assertEqual($expected_alias, \Drupal::service('path.alias_manager')->getAliasByPath($source, $langcode), t("Alias for %source with language '@language' is correct.", + array('%source' => $source, '@language' => $langcode))); + } + + public function assertAliasExists($conditions) { + $path = \Drupal::service('path.alias_storage')->load($conditions); + $this->assertTrue($path, t('Alias with conditions @conditions found.', array('@conditions' => var_export($conditions, TRUE)))); + return $path; + } + + public function assertNoAliasExists($conditions) { + $alias = \Drupal::service('path.alias_storage')->load($conditions); + $this->assertFalse($alias, t('Alias with conditions @conditions not found.', array('@conditions' => var_export($conditions, TRUE)))); + } + + public function deleteAllAliases() { + \Drupal::database()->delete('url_alias')->execute(); + \Drupal::service('path.alias_manager')->cacheClear(); + } + + /** + * @param array $values + * @return \Drupal\taxonomy\VocabularyInterface + */ + public function addVocabulary(array $values = array()) { + $name = Unicode::strtolower($this->randomMachineName(5)); + $values += array( + 'name' => $name, + 'vid' => $name, + ); + $vocabulary = Vocabulary::create($values); + $vocabulary->save(); + + return $vocabulary; + } + + public function addTerm(VocabularyInterface $vocabulary, array $values = array()) { + $values += array( + 'name' => Unicode::strtolower($this->randomMachineName(5)), + 'vid' => $vocabulary->id(), + ); + + $term = Term::create($values); + $term->save(); + return $term; + } + + public function assertEntityPattern($entity_type, $bundle, $langcode = Language::LANGCODE_NOT_SPECIFIED, $expected) { + + $values = [ + 'langcode' => $langcode, + \Drupal::entityTypeManager()->getDefinition($entity_type)->getKey('bundle') => $bundle, + ]; + $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->create($values); + + $pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity); + $this->assertIdentical($expected, $pattern->getPattern()); + } + + public function drupalGetTermByName($name, $reset = FALSE) { + if ($reset) { + // @todo - implement cache reset. + } + $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties(array('name' => $name)); + return !empty($terms) ? reset($terms) : FALSE; + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php new file mode 100644 index 000000000..a0bb43668 --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoUiTest.php @@ -0,0 +1,189 @@ +drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article')); + + // Allow other modules to add additional permissions for the admin user. + $permissions = array( + 'administer pathauto', + 'administer url aliases', + 'create url aliases', + 'administer nodes', + 'bypass node access', + 'access content overview', + ); + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + function testSettingsValidation() { + $edit = array(); + $edit['max_length'] = 'abc'; + $edit['max_component_length'] = 'abc'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + /*$this->assertText('The field Maximum alias length is not a valid number.'); + $this->assertText('The field Maximum component length is not a valid number.');*/ + $this->assertNoText('The configuration options have been saved.'); + + $edit['max_length'] = '0'; + $edit['max_component_length'] = '0'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + /*$this->assertText('The field Maximum alias length cannot be less than 1.'); + $this->assertText('The field Maximum component length cannot be less than 1.');*/ + $this->assertNoText('The configuration options have been saved.'); + + $edit['max_length'] = '999'; + $edit['max_component_length'] = '999'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + /*$this->assertText('The field Maximum alias length cannot be greater than 255.'); + $this->assertText('The field Maximum component length cannot be greater than 255.');*/ + $this->assertNoText('The configuration options have been saved.'); + + $edit['max_length'] = '50'; + $edit['max_component_length'] = '50'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + $this->assertText('The configuration options have been saved.'); + } + + function testPatternsWorkflow() { + // Try to save an empty pattern, should not be allowed. + $this->drupalGet('admin/config/search/path/patterns/add'); + $edit = array( + 'type' => 'canonical_entities:node', + ); + $this->drupalPostAjaxForm(NULL, $edit, 'type'); + $edit += array( + 'bundles[page]' => TRUE, + 'label' => 'Page pattern', + 'id' => 'page_pattern', + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Path pattern field is required.'); + $this->assertNoText('The configuration options have been saved.'); + + // Try to save an invalid pattern. + $edit += array( + 'pattern' => '[node:title]/[user:name]/[term:name]', + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Path pattern is using the following invalid tokens: [user:name], [term:name].'); + $this->assertNoText('The configuration options have been saved.'); + + $edit['pattern'] = '#[node:title]'; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('The Path pattern is using the following invalid characters: #.'); + $this->assertNoText('The configuration options have been saved.'); + + // Checking whitespace ending of the string. + $edit['pattern'] = '[node:title] '; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('The Path pattern doesn\'t allow the patterns ending with whitespace.'); + $this->assertNoText('The configuration options have been saved.'); + + // Fix the pattern, then check that it gets saved successfully. + $edit['pattern'] = '[node:title]'; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Pattern Page pattern saved.'); + + \Drupal::service('pathauto.generator')->resetCaches(); + + // Create a node with pattern enabled and check if the pattern applies. + $title = 'Page Pattern enabled'; + $alias = '/page-pattern-enabled'; + $node = $this->createNode(['title' => $title, 'type' => 'page']); + $this->drupalGet($alias); + $this->assertResponse(200); + $this->assertEntityAlias($node, $alias); + + // Edit workflow, set a new label and weight for the pattern. + $this->drupalPostForm('/admin/config/search/path/patterns', ['entities[page_pattern][weight]' => '4'], t('Save')); + $this->clickLink(t('Edit')); + $this->assertUrl('/admin/config/search/path/patterns/page_pattern'); + $this->assertFieldByName('pattern', '[node:title]'); + $this->assertFieldByName('label', 'Page pattern'); + $this->assertFieldChecked('edit-status'); + $this->assertLink(t('Delete')); + + $edit = array('label' => 'Test'); + $this->drupalPostForm('/admin/config/search/path/patterns/page_pattern', $edit, t('Save')); + $this->assertText('Pattern Test saved.'); + // Check that the pattern weight did not change. + $this->assertOptionSelected('edit-entities-page-pattern-weight', '4'); + + // Disable workflow. + $this->drupalGet('/admin/config/search/path/patterns'); + $this->assertNoLink(t('Enable')); + $this->clickLink(t('Disable')); + $this->assertUrl('/admin/config/search/path/patterns/page_pattern/disable'); + $this->drupalPostForm(NULL, [], t('Disable')); + $this->assertText('Disabled pattern Test.'); + + // Load the pattern from storage and check if its disabled. + $pattern = PathautoPattern::load('page_pattern'); + $this->assertFalse($pattern->status()); + + \Drupal::service('pathauto.generator')->resetCaches(); + + // Create a node with pattern disabled and check that we have no new alias. + $title = 'Page Pattern disabled'; + $node = $this->createNode(['title' => $title, 'type' => 'page']); + $this->assertNoEntityAlias($node); + + // Enable workflow. + $this->drupalGet('/admin/config/search/path/patterns'); + $this->assertNoLink(t('Disable')); + $this->clickLink(t('Enable')); + $this->assertUrl('/admin/config/search/path/patterns/page_pattern/enable'); + $this->drupalPostForm(NULL, [], t('Enable')); + $this->assertText('Enabled pattern Test.'); + + // Reload pattern from storage and check if its enabled. + $pattern = PathautoPattern::load('page_pattern'); + $this->assertTrue($pattern->status()); + + // Delete workflow. + $this->drupalGet('/admin/config/search/path/patterns'); + $this->clickLink(t('Delete')); + $this->assertUrl('/admin/config/search/path/patterns/page_pattern/delete'); + $this->assertText(t('This action cannot be undone.')); + $this->drupalPostForm(NULL, [], t('Delete')); + $this->assertText('The pathauto pattern Test has been deleted.'); + + $this->assertFalse(PathautoPattern::load('page_pattern')); + } + +} diff --git a/web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php b/web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php new file mode 100644 index 000000000..54832df9d --- /dev/null +++ b/web/modules/contrib/pathauto/src/Tests/PathautoUserWebTest.php @@ -0,0 +1,92 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + + $this->createPattern('user', '/users/[user:name]'); + } + + + /** + * Basic functional testing of Pathauto with users. + */ + function testUserEditing() { + // There should be no Pathauto checkbox on user forms. + $this->drupalGet('user/' . $this->adminUser->id() . '/edit'); + $this->assertNoFieldById('path[0][pathauto]'); + } + + /** + * Test user operations. + */ + function testUserOperations() { + $account = $this->drupalCreateUser(); + + // Delete all current URL aliases. + $this->deleteAllAliases(); + + // Find the position of just created account in the user_admin_people view. + $view = Views::getView('user_admin_people'); + $view->initDisplay(); + $view->preview('page_1'); + + + foreach ($view->result as $key => $row) { + if ($view->field['name']->getValue($row) == $account->getUsername()) { + break; + } + } + + $edit = array( + 'action' => 'pathauto_update_alias_user', + "user_bulk_form[$key]" => TRUE, + ); + $this->drupalPostForm('admin/people', $edit, t('Apply to selected items')); + $this->assertText('Update URL alias was applied to 1 item.'); + + $this->assertEntityAlias($account, '/users/' . Unicode::strtolower($account->getUsername())); + $this->assertEntityAlias($this->adminUser, '/user/' . $this->adminUser->id()); + } + +} diff --git a/web/modules/contrib/pathauto/src/VerboseMessenger.php b/web/modules/contrib/pathauto/src/VerboseMessenger.php new file mode 100644 index 000000000..bc60b5c90 --- /dev/null +++ b/web/modules/contrib/pathauto/src/VerboseMessenger.php @@ -0,0 +1,63 @@ +configFactory = $config_factory; + $this->account = $account; + } + + /** + * {@inheritdoc} + */ + public function addMessage($message, $op = NULL) { + + if (!isset($this->isVerbose)) { + $config = $this->configFactory->get('pathauto.settings'); + $this->isVerbose = $config->get('verbose') && $this->account->hasPermission('notify of path changes'); + } + + if (!$this->isVerbose || (isset($op) && in_array($op, array('bulkupdate', 'return')))) { + return FALSE; + } + + if ($message) { + drupal_set_message($message); + } + + return TRUE; + } + +} diff --git a/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/config/install/views.view.articles.yml b/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/config/install/views.view.articles.yml new file mode 100644 index 000000000..249516f5c --- /dev/null +++ b/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/config/install/views.view.articles.yml @@ -0,0 +1,231 @@ +langcode: en +status: true +dependencies: + module: + - node + - taxonomy + - user +id: articles +label: articles +module: pathauto_views_test +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + 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: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + row: + type: 'entity:node' + options: + view_mode: teaser + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + 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: '' + 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 + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + article: article + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: articles + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + tid: + id: tid + table: taxonomy_index + field: tid + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + add_table: false + require_value: false + reduce_duplicates: false + plugin_id: taxonomy_index_tid + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: articles + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml b/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml new file mode 100644 index 000000000..ad051ce13 --- /dev/null +++ b/web/modules/contrib/pathauto/tests/modules/pathauto_views_test/pathauto_views_test.info.yml @@ -0,0 +1,14 @@ +name: 'Views Test Config' +type: module +description: 'Provides default views for tests.' +package: Testing +# version: VERSION +# core: 8.x +dependencies: + - views + +# Information added by Drupal.org packaging script on 2017-04-29 +version: '8.x-1.0' +core: '8.x' +project: 'pathauto' +datestamp: 1493468049 diff --git a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php new file mode 100644 index 000000000..b69d4373c --- /dev/null +++ b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoKernelTest.php @@ -0,0 +1,572 @@ +installConfig(array('pathauto', 'taxonomy', 'system', 'node')); + + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installSchema('node', array('node_access')); + $this->installSchema('system', array('url_alias', 'sequences', 'router')); + + $type = NodeType::create(['type' => 'page']); + $type->save(); + node_add_body_field($type); + + $this->nodePattern = $this->createPattern('node', '/content/[node:title]'); + $this->userPattern = $this->createPattern('user', '/users/[user:name]'); + + \Drupal::service('router.builder')->rebuild(); + + $this->currentUser = User::create(array('name' => $this->randomMachineName())); + $this->currentUser->save(); + } + + /** + * Test _pathauto_get_schema_alias_maxlength(). + */ + public function testGetSchemaAliasMaxLength() { + $this->assertIdentical(\Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength(), 255); + } + + /** + * Test pathauto_pattern_load_by_entity(). + */ + public function testPatternLoadByEntity() { + $pattern = $this->createPattern('node', '/article/[node:title]', -1); + $this->addBundleCondition($pattern, 'node', 'article'); + $pattern->save(); + + $pattern = $this->createPattern('node', '/article/en/[node:title]', -2); + $this->addBundleCondition($pattern, 'node', 'article'); + $pattern->addSelectionCondition( + [ + 'id' => 'language', + 'langcodes' => [ + 'en' => 'en', + ], + 'negate' => FALSE, + 'context_mapping' => [ + 'language' => 'node:langcode:language', + ] + ] + ); + + $pattern->addRelationship('node:langcode:language'); + $pattern->save(); + + $pattern = $this->createPattern('node', '/[node:title]', -1); + $this->addBundleCondition($pattern, 'node', 'page'); + $pattern->save(); + + $tests = array( + array( + 'entity' => 'node', + 'values' => [ + 'title' => 'Article fr', + 'type' => 'article', + 'langcode' => 'fr', + ], + 'expected' => '/article/[node:title]', + ), + array( + 'entity' => 'node', + 'values' => [ + 'title' => 'Article en', + 'type' => 'article', + 'langcode' => 'en', + ], + 'expected' => '/article/en/[node:title]', + ), + array( + 'entity' => 'node', + 'values' => [ + 'title' => 'Article und', + 'type' => 'article', + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + 'expected' => '/article/[node:title]', + ), + array( + 'entity' => 'node', + 'values' => [ + 'title' => 'Page', + 'type' => 'page', + ], + 'expected' => '/[node:title]', + ), + array( + 'entity' => 'user', + 'values' => [ + 'name' => 'User', + ], + 'expected' => '/users/[user:name]', + ), + ); + foreach ($tests as $test) { + $entity = \Drupal::entityTypeManager()->getStorage($test['entity'])->create($test['values']); + $entity->save(); + $actual = \Drupal::service('pathauto.generator')->getPatternByEntity($entity); + $this->assertIdentical($actual->getPattern(), $test['expected'], t("Correct pattern returned for @entity_type with @values", array( + '@entity' => $test['entity'], + '@values' => print_r($test['values'], TRUE), + ))); + } + } + + /** + * Test potential conflicts with the same alias in different languages. + */ + public function testSameTitleDifferentLanguages() { + // Create two English articles with the same title. + $edit = [ + 'title' => 'Sample page', + 'type' => 'page', + 'langcode' => 'en', + ]; + $node1 = $this->drupalCreateNode($edit); + $this->assertEntityAlias($node1, '/content/sample-page', 'en'); + + $node2 = $this->drupalCreateNode($edit); + $this->assertEntityAlias($node2, '/content/sample-page-0', 'en'); + + // Now, create a French article with the same title, and verify that it gets + // the basic alias with the correct langcode. + $edit['langcode'] = 'fr'; + $node3 = $this->drupalCreateNode($edit); + $this->assertEntityAlias($node3, '/content/sample-page', 'fr'); + } + + /** + * Test pathauto_cleanstring(). + */ + public function testCleanString() { + + // Test with default settings defined in pathauto.settings.yml. + $this->installConfig(array('pathauto')); + \Drupal::service('pathauto.generator')->resetCaches(); + + $tests = array(); + + // Test the 'ignored words' removal. + $tests['this'] = 'this'; + $tests['this with that'] = 'this-with-that'; + $tests['this thing with that thing'] = 'thing-thing'; + + // Test 'ignored words' removal and duplicate separator removal. + $tests[' - Pathauto is the greatest - module ever - '] = 'pathauto-greatest-module-ever'; + + // Test length truncation and lowering of strings. + $long_string = $this->randomMachineName(120); + $tests[$long_string] = strtolower(substr($long_string, 0, 100)); + + // Test that HTML tags are removed. + $tests['This text has
HTML tags.'] = 'text-has-html-tags'; + $tests[Html::escape('This text has
HTML tags.')] = 'text-has-html-tags'; + + // Transliteration. + $tests['ľščťžýáíéňô'] = 'lsctzyaieno'; + + foreach ($tests as $input => $expected) { + $output = \Drupal::service('pathauto.alias_cleaner')->cleanString($input); + $this->assertEqual($output, $expected, t("Drupal::service('pathauto.alias_cleaner')->cleanString('@input') expected '@expected', actual '@output'", array( + '@input' => $input, + '@expected' => $expected, + '@output' => $output, + ))); + } + } + + /** + * Test pathauto_clean_alias(). + */ + public function testCleanAlias() { + $tests = array(); + $tests['one/two/three'] = '/one/two/three'; + $tests['/one/two/three/'] = '/one/two/three'; + $tests['one//two///three'] = '/one/two/three'; + $tests['one/two--three/-/--/-/--/four---five'] = '/one/two-three/four-five'; + $tests['one/-//three--/four'] = '/one/three/four'; + + foreach ($tests as $input => $expected) { + $output = \Drupal::service('pathauto.alias_cleaner')->cleanAlias($input); + $this->assertEqual($output, $expected, t("Drupal::service('pathauto.generator')->cleanAlias('@input') expected '@expected', actual '@output'", array( + '@input' => $input, + '@expected' => $expected, + '@output' => $output, + ))); + } + } + + /** + * Test pathauto_path_delete_multiple(). + */ + public function testPathDeleteMultiple() { + $this->saveAlias('/node/1', '/node-1-alias'); + $this->saveAlias('/node/1/view', '/node-1-alias/view'); + $this->saveAlias('/node/1', '/node-1-alias-en', 'en'); + $this->saveAlias('/node/1', '/node-1-alias-fr', 'fr'); + $this->saveAlias('/node/2', '/node-2-alias'); + $this->saveAlias('/node/10', '/node-10-alias'); + + \Drupal::service('pathauto.alias_storage_helper')->deleteBySourcePrefix('/node/1'); + $this->assertNoAliasExists(array('source' => "/node/1")); + $this->assertNoAliasExists(array('source' => "/node/1/view")); + $this->assertAliasExists(array('source' => "/node/2")); + $this->assertAliasExists(array('source' => "/node/10")); + } + + /** + * Test the different update actions in \Drupal::service('pathauto.generator')->createEntityAlias(). + */ + public function testUpdateActions() { + $config = $this->config('pathauto.settings'); + + // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'insert'. + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW); + $config->save(); + $node = $this->drupalCreateNode(array('title' => 'First title')); + $this->assertEntityAlias($node, '/content/first-title'); + + $node->path->pathauto = PathautoState::CREATE; + + // Default action is PATHAUTO_UPDATE_ACTION_DELETE. + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_DELETE); + $config->save(); + $node->setTitle('Second title'); + $node->save(); + $this->assertEntityAlias($node, '/content/second-title'); + $this->assertNoAliasExists(array('alias' => '/content/first-title')); + + // Test PATHAUTO_UPDATE_ACTION_LEAVE + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_LEAVE); + $config->save(); + $node->setTitle('Third title'); + $node->save(); + $this->assertEntityAlias($node, '/content/third-title'); + $this->assertAliasExists(array('source' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title')); + + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_DELETE); + $config->save(); + $node->setTitle('Fourth title'); + $node->save(); + $this->assertEntityAlias($node, '/content/fourth-title'); + $this->assertNoAliasExists(array('alias' => '/content/third-title')); + // The older second alias is not deleted yet. + $older_path = $this->assertAliasExists(array('source' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title')); + \Drupal::service('path.alias_storage')->delete($older_path); + + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW); + $config->save(); + $node->setTitle('Fifth title'); + $node->save(); + $this->assertEntityAlias($node, '/content/fourth-title'); + $this->assertNoAliasExists(array('alias' => '/content/fifth-title')); + + // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'update'. + $this->deleteAllAliases(); + $node->save(); + $this->assertEntityAlias($node, '/content/fifth-title'); + + // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'bulkupdate'. + $this->deleteAllAliases(); + $node->setTitle('Sixth title'); + \Drupal::service('pathauto.generator')->updateEntityAlias($node, 'bulkupdate'); + $this->assertEntityAlias($node, '/content/sixth-title'); + } + + /** + * Test that \Drupal::service('pathauto.generator')->createEntityAlias() will not create an alias for a pattern + * that does not get any tokens replaced. + */ + public function testNoTokensNoAlias() { + $this->installConfig(['filter']); + $this->nodePattern + ->setPattern('/content/[node:body]') + ->save(); + + $node = $this->drupalCreateNode(); + $this->assertNoEntityAliasExists($node); + + $node->body->value = 'hello'; + $node->save(); + $this->assertEntityAlias($node, '/content/hello'); + } + + /** + * Test the handling of path vs non-path tokens in pathauto_clean_token_values(). + */ + public function testPathTokens() { + $this->createPattern('taxonomy_term', '/[term:parent:url:path]/[term:name]'); + + $vocab = $this->addVocabulary(); + + $term1 = $this->addTerm($vocab, array('name' => 'Parent term')); + $this->assertEntityAlias($term1, '/parent-term'); + + $term2 = $this->addTerm($vocab, array('name' => 'Child term', 'parent' => $term1->id())); + $this->assertEntityAlias($term2, '/parent-term/child-term'); + + $this->saveEntityAlias($term1, '/My Crazy/Alias/'); + $term2->save(); + $this->assertEntityAlias($term2, '/My Crazy/Alias/child-term'); + } + + /** + * Test using fields for path structures. + */ + function testParentChildPathTokens() { + // First create a field which will be used to create the path. It must + // begin with a letter. + + $this->installEntitySchema('taxonomy_term'); + + Vocabulary::create(['vid' => 'tags'])->save(); + + $fieldname = 'a' . Unicode::strtolower($this->randomMachineName()); + $field_storage = FieldStorageConfig::create(['entity_type' => 'taxonomy_term', 'field_name' => $fieldname, 'type' => 'string']); + $field_storage->save(); + $field = FieldConfig::create(['field_storage' => $field_storage, 'bundle' => 'tags']); + $field->save(); + + $display = entity_get_display('taxonomy_term', 'tags', 'default'); + $display->setComponent($fieldname, ['type' => 'string']); + $display->save(); + + // Make the path pattern of a field use the value of this field appended + // to the parent taxonomy term's pattern if there is one. + $this->createPattern('taxonomy_term', '/[term:parents:join-path]/[term:' . $fieldname . ']'); + + // Start by creating a parent term. + $parent = Term::create(['vid' => 'tags', $fieldname => $this->randomMachineName(), 'name' => $this->randomMachineName()]); + $parent->save(); + + // Create the child term. + $child = Term::create(['vid' => 'tags', $fieldname => $this->randomMachineName(), 'parent' => $parent, 'name' => $this->randomMachineName()]); + $child->save(); + $this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value)); + + // Re-saving the parent term should not modify the child term's alias. + $parent->save(); + $this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value)); + } + + /** + * Tests aliases on taxonomy terms. + */ + public function testTaxonomyPattern() { + // Create a vocabulary and test that it's pattern variable works. + $vocab = $this->addVocabulary(array('vid' => 'name')); + $this->createPattern('taxonomy_term', 'base'); + $pattern = $this->createPattern('taxonomy_term', 'bundle', -1); + $this->addBundleCondition($pattern, 'taxonomy_term', 'name'); + $pattern->save(); + $this->assertEntityPattern('taxonomy_term', 'name', Language::LANGCODE_NOT_SPECIFIED, 'bundle'); + } + + function testNoExistingPathAliases() { + $this->config('pathauto.settings') + ->set('punctuation.period', PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING) + ->save(); + + $this->nodePattern + ->setPattern('[node:title]') + ->save(); + + // Check that Pathauto does not create an alias of '/admin'. + $node = $this->drupalCreateNode(array('title' => 'Admin', 'type' => 'page')); + $this->assertEntityAlias($node, '/admin-0'); + + // Check that Pathauto does not create an alias of '/modules'. + $node->setTitle('Modules'); + $node->save(); + $this->assertEntityAlias($node, '/modules-0'); + + // Check that Pathauto does not create an alias of '/index.php'. + $node->setTitle('index.php'); + $node->save(); + $this->assertEntityAlias($node, '/index.php-0'); + + // Check that a safe value gets an automatic alias. This is also a control + // to ensure the above tests work properly. + $node->setTitle('Safe value'); + $node->save(); + $this->assertEntityAlias($node, '/safe-value'); + } + + /** + * Test programmatic entity creation for aliases. + */ + function testProgrammaticEntityCreation() { + $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]'); + $node = $this->drupalCreateNode(array('title' => 'Test node', 'path' => array('pathauto' => TRUE))); + $this->assertEntityAlias($node, '/content/test-node'); + + $vocabulary = $this->addVocabulary(array('name' => 'Tags')); + $term = $this->addTerm($vocabulary, array('name' => 'Test term', 'path' => array('pathauto' => TRUE))); + $this->assertEntityAlias($term, '/tags/test-term'); + + $edit['name'] = 'Test user'; + $edit['mail'] = 'test-user@example.com'; + $edit['pass'] = user_password(); + $edit['path'] = array('pathauto' => TRUE); + $edit['status'] = 1; + $account = User::create($edit); + $account->save(); + $this->assertEntityAlias($account, '/users/test-user'); + } + + /** + * Tests word safe alias truncating. + */ + function testPathAliasUniquifyWordsafe() { + $this->config('pathauto.settings') + ->set('max_length', 26) + ->save(); + + $node_1 = $this->drupalCreateNode(array('title' => 'thequick brownfox jumpedover thelazydog', 'type' => 'page')); + $node_2 = $this->drupalCreateNode(array('title' => 'thequick brownfox jumpedover thelazydog', 'type' => 'page')); + + // Check that alias uniquifying is truncating with $wordsafe param set to + // TRUE. + // If it doesn't path alias result would be content/thequick-brownf-0 + $this->assertEntityAlias($node_1, '/content/thequick-brownfox'); + $this->assertEntityAlias($node_2, '/content/thequick-0'); + } + + /** + * Test if aliases are (not) generated with enabled/disabled patterns. + */ + function testPatternStatus() { + // Create a node to get an alias for. + $title = 'Pattern enabled'; + $alias = '/content/pattern-enabled'; + $node1 = $this->drupalCreateNode(['title' => $title, 'type' => 'page']); + $this->assertEntityAlias($node1, $alias); + + // Disable the pattern, save the node again and make sure the alias is still + // working. + $this->nodePattern->setStatus(FALSE)->save(); + + $node1->save(); + $this->assertEntityAlias($node1, $alias); + + // Create a new node with disabled pattern and make sure there is no new + // alias created. + $title = 'Pattern disabled'; + $node2 = $this->drupalCreateNode(['title' => $title, 'type' => 'page']); + $this->assertNoEntityAlias($node2); + } + + /** + * Tests that enabled entity types genrates the necessary fields and plugins. + */ + public function testSettingChangeInvalidatesCache() { + + $this->installConfig(['pathauto']); + + $this->enableModules(['entity_test']); + + $definitions = \Drupal::service('plugin.manager.alias_type')->getDefinitions(); + $this->assertFalse(isset($definitions['canonical_entities:entity_test'])); + + $fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('entity_test'); + $this->assertFalse(isset($fields['path'])); + + $this->config('pathauto.settings') + ->set('enabled_entity_types', ['user', 'entity_test']) + ->save(); + + $definitions = \Drupal::service('plugin.manager.alias_type')->getDefinitions(); + $this->assertTrue(isset($definitions['canonical_entities:entity_test'])); + + $fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('entity_test'); + $this->assertTrue(isset($fields['path'])); + + } + + /** + * Tests that aliases are only generated for default revisions. + */ + public function testDefaultRevision() { + $node1 = $this->drupalCreateNode(['title' => 'Default revision', 'type' => 'page']); + $this->assertEntityAlias($node1, '/content/default-revision'); + + $node1->setNewRevision(TRUE); + $node1->isDefaultRevision(FALSE); + $node1->setTitle('New non-default-revision'); + $node1->save(); + + $this->assertEntityAlias($node1, '/content/default-revision'); + } + + /** + * Creates a node programmatically. + * + * @param array $settings + * The array of values for the node. + * + * @return \Drupal\node\Entity\Node + * The created node. + */ + protected function drupalCreateNode(array $settings = array()) { + // Populate defaults array. + $settings += array( + 'title' => $this->randomMachineName(8), + 'type' => 'page', + ); + + $node = Node::create($settings); + $node->save(); + + return $node; + } + +} diff --git a/web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php new file mode 100644 index 000000000..30c5ad1d2 --- /dev/null +++ b/web/modules/contrib/pathauto/tests/src/Kernel/PathautoTokenTest.php @@ -0,0 +1,78 @@ +installConfig(array('pathauto')); + + $array = array( + 'test first arg', + 'The Array / value', + ); + + $tokens = array( + 'join-path' => 'test-first-arg/array-value', + ); + $data['array'] = $array; + $replacements = $this->assertTokens('array', $data, $tokens); + + // Ensure that the cleanTokenValues() method does not alter this token value. + /* @var \Drupal\pathauto\AliasCleanerInterface $alias_cleaner */ + $alias_cleaner = \Drupal::service('pathauto.alias_cleaner'); + $alias_cleaner->cleanTokenValues($replacements, $data, array()); + $this->assertEqual($replacements['[array:join-path]'], 'test-first-arg/array-value'); + } + + /** + * Function copied from TokenTestHelper::assertTokens(). + */ + public function assertTokens($type, array $data, array $tokens, array $options = array()) { + $input = $this->mapTokenNames($type, array_keys($tokens)); + $bubbleable_metadata = new BubbleableMetadata(); + $replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata); + foreach ($tokens as $name => $expected) { + $token = $input[$name]; + if (!isset($expected)) { + $this->assertTrue(!isset($values[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); + } + elseif (!isset($replacements[$token])) { + $this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); + } + elseif (!empty($options['regex'])) { + $this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); + } + else { + $this->assertIdentical($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); + } + } + + return $replacements; + } + + public function mapTokenNames($type, array $tokens = array()) { + $return = array(); + foreach ($tokens as $token) { + $return[$token] = "[$type:$token]"; + } + return $return; + } + +} diff --git a/web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php b/web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php new file mode 100644 index 000000000..9567dd54b --- /dev/null +++ b/web/modules/contrib/pathauto/tests/src/Unit/VerboseMessengerTest.php @@ -0,0 +1,59 @@ +getConfigFactoryStub(array('pathauto.settings' => array('verbose' => TRUE))); + $account = $this->getMock('\Drupal\Core\Session\AccountInterface'); + $account->expects($this->once()) + ->method('hasPermission') + ->withAnyParameters() + ->willReturn(TRUE); + + $this->messenger = new VerboseMessenger($config_factory, $account); + } + + /** + * Tests add messages. + * @covers ::addMessage + */ + public function testAddMessage() { + $this->assertTrue($this->messenger->addMessage("Test message"), "The message was added"); + } + + /** + * @covers ::addMessage + */ + public function testDoNotAddMessageWhileBulkupdate() { + $this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added"); + } + +} + +} +namespace { + // @todo Delete after https://drupal.org/node/1858196 is in. + if (!function_exists('drupal_set_message')) { + function drupal_set_message() { + } + } +} diff --git a/web/modules/contrib/token/.travis.yml b/web/modules/contrib/token/.travis.yml new file mode 100644 index 000000000..fbf503605 --- /dev/null +++ b/web/modules/contrib/token/.travis.yml @@ -0,0 +1,96 @@ +language: php +cache: + bundler: true + directories: + - $HOME/tmp/drush + - $HOME/.bundle + apt: true + +php: + - 5.4 + - 5.5 + +env: + - PATH=$PATH:/home/travis/.composer/vendor/bin + +# This will create the database +mysql: + database: drupal + username: root + encoding: utf8 + +# To be able to run a webbrowser +# If we need anything more powerful +# than e.g. phantomjs +before_install: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + +install: + # Grab Drush + - composer global require drush/drush:dev-master --prefer-source + - cd /home/travis/.composer/vendor/drush/drush && cd - + # Make sure we don't fail when checking out projects + - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config + # LAMP package installation (mysql is already started) + - sudo apt-get update + - sudo apt-get install apache2 libapache2-mod-fastcgi + # enable php-fpm, travis does not support any other method with php and apache + - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf + - sudo a2enmod rewrite actions fastcgi alias + - echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm + # Make sure the apache root is in our wanted directory + - echo "$(curl -fsSL https://gist.githubusercontent.com/nickveenhof/11386315/raw/b8abaf9304fe12b5cc7752d39c29c1edae8ac2e6/gistfile1.txt)" | sed -e "s,PATH,$TRAVIS_BUILD_DIR/../drupal,g" | sudo tee /etc/apache2/sites-available/default > /dev/null + # Set sendmail so drush doesn't throw an error during site install. + - echo "sendmail_path='true'" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` + # Forward the errors to the syslog so we can print them + - echo "error_log=syslog" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` + # Get latest drupal 8 core + - cd $TRAVIS_BUILD_DIR/.. + - git clone --depth 1 --branch 8.0.x http://git.drupal.org/project/drupal.git + # Restart apache and test it + - sudo service apache2 restart + - curl -v "http://localhost" + # Re-enable when trying to get CodeSniffer doesn't return a 403 anymore. + #- composer global require drupal/coder:\>7 + +before_script: + - cd $TRAVIS_BUILD_DIR/../drupal + # Update drupal core + - git pull origin 8.0.x + # Install the site + - drush -v site-install minimal --db-url=mysql://root:@localhost/drupal --yes + - drush en --yes simpletest + - drush cr + - phpenv rehash + +script: + # go to our Drupal module directory + - mkdir $TRAVIS_BUILD_DIR/../drupal/modules/token + - cp -R $TRAVIS_BUILD_DIR/* $TRAVIS_BUILD_DIR/../drupal/modules/token/ + # go to our Drupal main directory + - cd $TRAVIS_BUILD_DIR/../drupal + - ls -la $TRAVIS_BUILD_DIR/../drupal/sites/default + # Run the tests + - php core/scripts/run-tests.sh --verbose --color --concurrency 4 --php `which php` --url http://localhost "token" | tee /tmp/test.txt; TEST_EXIT=${PIPESTATUS[0]} + - echo $TEST_EXIT + # Check if we had fails in the run-tests.sh script + # Exit with the inverted value, because if there are no fails found, it will exit with 1 and for us that\ + # is a good thing so invert it to 0. Travis has some issues with the exclamation mark in front so we have to fiddle a + # bit. + # Also make the grep case insensitive and fail on run-tests.sh regular fails as well on fatal errors. + - TEST_OUTPUT=$(! egrep -i "([0-9]+ fails)|(PHP Fatal error)|([0-9]+ exceptions)" /tmp/test.txt > /dev/null)$? + - echo $TEST_OUTPUT + - cd $TRAVIS_BUILD_DIR/../drupal/core + - ./vendor/bin/phpunit --verbose --debug ../modules/token/; TEST_PHPUNIT=$? + - echo $TEST_PHPUNIT + # if the TEST_EXIT status is 0 AND the TEST_OUTPUT status is also 0 it means we succeeded, in all other cases we + # failed. + # Re-enable when trying to get CodeSniffer doesn't return a 403 anymore. + #- /home/travis/.composer/vendor/bin/phpcs --standard=/home/travis/.composer/vendor/drupal/coder/coder_sniffer/Drupal --extensions=php,inc,test,module,install --ignore=css/ $TRAVIS_BUILD_DIR/../drupal/modules/search_api + - php -i | grep 'php.ini' + - sudo cat /var/log/apache2/error.log + - sudo cat /var/log/syslog | grep 'php' | cat # Suppress grep exit status 1 + # Exit the build + - if [ $TEST_EXIT -eq 0 ] && [ $TEST_OUTPUT -eq 0 ] && [ $TEST_PHPUNIT -eq 0 ]; then exit 0; else exit 1; fi diff --git a/web/modules/contrib/token/LICENSE.txt b/web/modules/contrib/token/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/web/modules/contrib/token/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/web/modules/contrib/token/README.md b/web/modules/contrib/token/README.md new file mode 100644 index 000000000..c37d33508 --- /dev/null +++ b/web/modules/contrib/token/README.md @@ -0,0 +1,33 @@ +INTRODUCTION +------------ + + Provides common and resuable token UI elements and missing core tokens. + + * For a full description of the module, visit the project page: + https://drupal.org/project/token + + * To submit bug reports and feature suggestions, or to track changes: + https://drupal.org/project/issues/token + + +INSTALLATION +------------ + +Install as usual, see + https://www.drupal.org/docs/8/extending-drupal-8/installing-contributed-modules-find-import-enable-configure-drupal-8 for further +information. + + +TROUBLESHOOTING +--------------- + +Token module doesn't provide any visible functions to the user on its own, it +just provides token handling services for other modules. + + +MAINTAINERS +----------- + +Current maintainers: + + * Dave Reid (https://drupal.org/user/53892) diff --git a/web/modules/contrib/token/css/jquery.treetable.css b/web/modules/contrib/token/css/jquery.treetable.css new file mode 100644 index 000000000..4e95bfd3a --- /dev/null +++ b/web/modules/contrib/token/css/jquery.treetable.css @@ -0,0 +1,28 @@ +table.treetable span.indenter { + display: inline-block; + margin: 0; + padding: 0; + text-align: right; + + /* Disable text selection of nodes (for better D&D UX) */ + user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + -webkit-user-select: none; + + /* Force content-box box model for indenter (Bootstrap compatibility) */ + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + + width: 19px; +} + +table.treetable span.indenter a { + background-position: left center; + background-repeat: no-repeat; + display: inline-block; + text-decoration: none; + width: 19px; +} diff --git a/web/modules/contrib/token/css/token.css b/web/modules/contrib/token/css/token.css new file mode 100644 index 000000000..ca201b8ac --- /dev/null +++ b/web/modules/contrib/token/css/token.css @@ -0,0 +1,32 @@ +.token-tree-dialog { + border: 1px solid #6b6b6b; + border-radius: 5px 5px 0 0; + box-shadow: 0 0 10px #6b6b6b; +} + +.token-tree { + font-size: 0.85em; + margin-left: 19px; +} + +.ui-dialog-content .token-tree { + margin-left: 0; +} + +.token-tree td, .token-tree th { + padding-top: 0; + padding-bottom: 0; +} + +.token-group { + font-weight: bold; +} + +.js .token-group { + font-weight: normal; +} + +/* Prevent the token columns from being wrapped. */ +.token-tree td.token-key { + white-space: nowrap; +} diff --git a/web/modules/contrib/token/css/token.treetable.theme.css b/web/modules/contrib/token/css/token.treetable.theme.css new file mode 100644 index 000000000..a93c557f0 --- /dev/null +++ b/web/modules/contrib/token/css/token.treetable.theme.css @@ -0,0 +1,50 @@ +table.treetable { +} + +table.treetable caption { +} + +table.treetable thead { +} + +table.treetable thead tr th { +} + +table.treetable tbody tr td { + cursor: default; +} + +table.treetable span { + background-position: center left; + background-repeat: no-repeat; + padding: .2em 0 .2em 1.5em; +} + +table.treetable tr.collapsed span.indenter a { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHlJREFUeNrcU1sNgDAQ6wgmcAM2MICGGlg1gJnNzWQcvwQGy1j4oUl/7tH0mpwzM7SgQyO+EZAUWh2MkkzSWhJwuRAlHYsJwEwyvs1gABDuzqoJcTw5qxaIJN0bgQRgIjnlmn1heSO5PE6Y2YXe+5Cr5+h++gs12AcAS6FS+7YOsj4AAAAASUVORK5CYII=); +} + +table.treetable tr.expanded span.indenter a { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHFJREFUeNpi/P//PwMlgImBQsA44C6gvhfa29v3MzAwOODRc6CystIRbxi0t7fjDJjKykpGYrwwi1hxnLHQ3t7+jIGBQRJJ6HllZaUUKYEYRYBPOB0gBShKwKGA////48VtbW3/8clTnBIH3gCKkzJgAGvBX0dDm0sCAAAAAElFTkSuQmCC); +} + +table.treetable tr.branch { + background-color: #f9f9f9; +} + +table.treetable tr.selected { + background-color: #3875d7; + color: #fff; +} + +table.treetable tr span.indenter { + margin-left: -19px; +} +table.treetable tr span.indenter a { + outline: none; /* Expander shows outline after upgrading to 3.0 (#141) */ +} + +table.treetable tr.accept { + background-color: #a3bce4; + color: #fff +} diff --git a/web/modules/contrib/token/js/jquery.treetable.js b/web/modules/contrib/token/js/jquery.treetable.js new file mode 100644 index 000000000..ac9d90ac3 --- /dev/null +++ b/web/modules/contrib/token/js/jquery.treetable.js @@ -0,0 +1,629 @@ +/* + * jQuery treetable Plugin 3.2.0 + * http://ludo.cubicphuse.nl/jquery-treetable + * + * Copyright 2013, Ludo van den Boom + * Dual licensed under the MIT or GPL Version 2 licenses. + */ +(function($) { + var Node, Tree, methods; + + Node = (function() { + function Node(row, tree, settings) { + var parentId; + + this.row = row; + this.tree = tree; + this.settings = settings; + + // TODO Ensure id/parentId is always a string (not int) + this.id = this.row.data(this.settings.nodeIdAttr); + + // TODO Move this to a setParentId function? + parentId = this.row.data(this.settings.parentIdAttr); + if (parentId != null && parentId !== "") { + this.parentId = parentId; + } + + this.treeCell = $(this.row.children(this.settings.columnElType)[this.settings.column]); + this.expander = $(this.settings.expanderTemplate); + this.indenter = $(this.settings.indenterTemplate); + this.children = []; + this.initialized = false; + this.treeCell.prepend(this.indenter); + } + + Node.prototype.addChild = function(child) { + return this.children.push(child); + }; + + Node.prototype.ancestors = function() { + var ancestors, node; + node = this; + ancestors = []; + while (node = node.parentNode()) { + ancestors.push(node); + } + return ancestors; + }; + + Node.prototype.collapse = function() { + if (this.collapsed()) { + return this; + } + + this.row.removeClass("expanded").addClass("collapsed"); + + this._hideChildren(); + this.expander.attr("title", this.settings.stringExpand); + + if (this.initialized && this.settings.onNodeCollapse != null) { + this.settings.onNodeCollapse.apply(this); + } + + return this; + }; + + Node.prototype.collapsed = function() { + return this.row.hasClass("collapsed"); + }; + + // TODO destroy: remove event handlers, expander, indenter, etc. + + Node.prototype.expand = function() { + if (this.expanded()) { + return this; + } + + this.row.removeClass("collapsed").addClass("expanded"); + + if (this.initialized && this.settings.onNodeExpand != null) { + this.settings.onNodeExpand.apply(this); + } + + if ($(this.row).is(":visible")) { + this._showChildren(); + } + + this.expander.attr("title", this.settings.stringCollapse); + + return this; + }; + + Node.prototype.expanded = function() { + return this.row.hasClass("expanded"); + }; + + Node.prototype.hide = function() { + this._hideChildren(); + this.row.hide(); + return this; + }; + + Node.prototype.isBranchNode = function() { + if(this.children.length > 0 || this.row.data(this.settings.branchAttr) === true) { + return true; + } else { + return false; + } + }; + + Node.prototype.updateBranchLeafClass = function(){ + this.row.removeClass('branch'); + this.row.removeClass('leaf'); + this.row.addClass(this.isBranchNode() ? 'branch' : 'leaf'); + }; + + Node.prototype.level = function() { + return this.ancestors().length; + }; + + Node.prototype.parentNode = function() { + if (this.parentId != null) { + return this.tree[this.parentId]; + } else { + return null; + } + }; + + Node.prototype.removeChild = function(child) { + var i = $.inArray(child, this.children); + return this.children.splice(i, 1) + }; + + Node.prototype.render = function() { + var handler, + settings = this.settings, + target; + + if (settings.expandable === true && this.isBranchNode()) { + handler = function(e) { + $(this).parents("table").treetable("node", $(this).parents("tr").data(settings.nodeIdAttr)).toggle(); + return e.preventDefault(); + }; + + this.indenter.html(this.expander); + target = settings.clickableNodeNames === true ? this.treeCell : this.expander; + + target.off("click.treetable").on("click.treetable", handler); + target.off("keydown.treetable").on("keydown.treetable", function(e) { + if (e.keyCode == 13) { + handler.apply(this, [e]); + } + }); + } + + this.indenter[0].style.paddingLeft = "" + (this.level() * settings.indent) + "px"; + + return this; + }; + + Node.prototype.reveal = function() { + if (this.parentId != null) { + this.parentNode().reveal(); + } + return this.expand(); + }; + + Node.prototype.setParent = function(node) { + if (this.parentId != null) { + this.tree[this.parentId].removeChild(this); + } + this.parentId = node.id; + this.row.data(this.settings.parentIdAttr, node.id); + return node.addChild(this); + }; + + Node.prototype.show = function() { + if (!this.initialized) { + this._initialize(); + } + this.row.show(); + if (this.expanded()) { + this._showChildren(); + } + return this; + }; + + Node.prototype.toggle = function() { + if (this.expanded()) { + this.collapse(); + } else { + this.expand(); + } + return this; + }; + + Node.prototype._hideChildren = function() { + var child, _i, _len, _ref, _results; + _ref = this.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + _results.push(child.hide()); + } + return _results; + }; + + Node.prototype._initialize = function() { + var settings = this.settings; + + this.render(); + + if (settings.expandable === true && settings.initialState === "collapsed") { + this.collapse(); + } else { + this.expand(); + } + + if (settings.onNodeInitialized != null) { + settings.onNodeInitialized.apply(this); + } + + return this.initialized = true; + }; + + Node.prototype._showChildren = function() { + var child, _i, _len, _ref, _results; + _ref = this.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + _results.push(child.show()); + } + return _results; + }; + + return Node; + })(); + + Tree = (function() { + function Tree(table, settings) { + this.table = table; + this.settings = settings; + this.tree = {}; + + // Cache the nodes and roots in simple arrays for quick access/iteration + this.nodes = []; + this.roots = []; + } + + Tree.prototype.collapseAll = function() { + var node, _i, _len, _ref, _results; + _ref = this.nodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.collapse()); + } + return _results; + }; + + Tree.prototype.expandAll = function() { + var node, _i, _len, _ref, _results; + _ref = this.nodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.expand()); + } + return _results; + }; + + Tree.prototype.findLastNode = function (node) { + if (node.children.length > 0) { + return this.findLastNode(node.children[node.children.length - 1]); + } else { + return node; + } + }; + + Tree.prototype.loadRows = function(rows) { + var node, row, i; + + if (rows != null) { + for (i = 0; i < rows.length; i++) { + row = $(rows[i]); + + if (row.data(this.settings.nodeIdAttr) != null) { + node = new Node(row, this.tree, this.settings); + this.nodes.push(node); + this.tree[node.id] = node; + + if (node.parentId != null && this.tree[node.parentId]) { + this.tree[node.parentId].addChild(node); + } else { + this.roots.push(node); + } + } + } + } + + for (i = 0; i < this.nodes.length; i++) { + node = this.nodes[i].updateBranchLeafClass(); + } + + return this; + }; + + Tree.prototype.move = function(node, destination) { + // Conditions: + // 1: +node+ should not be inserted as a child of +node+ itself. + // 2: +destination+ should not be the same as +node+'s current parent (this + // prevents +node+ from being moved to the same location where it already + // is). + // 3: +node+ should not be inserted in a location in a branch if this would + // result in +node+ being an ancestor of itself. + var nodeParent = node.parentNode(); + if (node !== destination && destination.id !== node.parentId && $.inArray(node, destination.ancestors()) === -1) { + node.setParent(destination); + this._moveRows(node, destination); + + // Re-render parentNode if this is its first child node, and therefore + // doesn't have the expander yet. + if (node.parentNode().children.length === 1) { + node.parentNode().render(); + } + } + + if(nodeParent){ + nodeParent.updateBranchLeafClass(); + } + if(node.parentNode()){ + node.parentNode().updateBranchLeafClass(); + } + node.updateBranchLeafClass(); + return this; + }; + + Tree.prototype.removeNode = function(node) { + // Recursively remove all descendants of +node+ + this.unloadBranch(node); + + // Remove node from DOM () + node.row.remove(); + + // Remove node from parent children list + if (node.parentId != null) { + node.parentNode().removeChild(node); + } + + // Clean up Tree object (so Node objects are GC-ed) + delete this.tree[node.id]; + this.nodes.splice($.inArray(node, this.nodes), 1); + + return this; + } + + Tree.prototype.render = function() { + var root, _i, _len, _ref; + _ref = this.roots; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + root = _ref[_i]; + + // Naming is confusing (show/render). I do not call render on node from + // here. + root.show(); + } + return this; + }; + + Tree.prototype.sortBranch = function(node, sortFun) { + // First sort internal array of children + node.children.sort(sortFun); + + // Next render rows in correct order on page + this._sortChildRows(node); + + return this; + }; + + Tree.prototype.unloadBranch = function(node) { + // Use a copy of the children array to not have other functions interfere + // with this function if they manipulate the children array + // (eg removeNode). + var children = node.children.slice(0), + i; + + for (i = 0; i < children.length; i++) { + this.removeNode(children[i]); + } + + // Reset node's collection of children + node.children = []; + + node.updateBranchLeafClass(); + + return this; + }; + + Tree.prototype._moveRows = function(node, destination) { + var children = node.children, i; + + node.row.insertAfter(destination.row); + node.render(); + + // Loop backwards through children to have them end up on UI in correct + // order (see #112) + for (i = children.length - 1; i >= 0; i--) { + this._moveRows(children[i], node); + } + }; + + // Special _moveRows case, move children to itself to force sorting + Tree.prototype._sortChildRows = function(parentNode) { + return this._moveRows(parentNode, parentNode); + }; + + return Tree; + })(); + + // jQuery Plugin + methods = { + init: function(options, force) { + var settings; + + settings = $.extend({ + branchAttr: "ttBranch", + clickableNodeNames: false, + column: 0, + columnElType: "td", // i.e. 'td', 'th' or 'td,th' + expandable: false, + expanderTemplate: " ", + indent: 19, + indenterTemplate: "", + initialState: "collapsed", + nodeIdAttr: "ttId", // maps to data-tt-id + parentIdAttr: "ttParentId", // maps to data-tt-parent-id + stringExpand: "Expand", + stringCollapse: "Collapse", + + // Events + onInitialized: null, + onNodeCollapse: null, + onNodeExpand: null, + onNodeInitialized: null + }, options); + + return this.each(function() { + var el = $(this), tree; + + if (force || el.data("treetable") === undefined) { + tree = new Tree(this, settings); + tree.loadRows(this.rows).render(); + + el.addClass("treetable").data("treetable", tree); + + if (settings.onInitialized != null) { + settings.onInitialized.apply(tree); + } + } + + return el; + }); + }, + + destroy: function() { + return this.each(function() { + return $(this).removeData("treetable").removeClass("treetable"); + }); + }, + + collapseAll: function() { + this.data("treetable").collapseAll(); + return this; + }, + + collapseNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + node.collapse(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + expandAll: function() { + this.data("treetable").expandAll(); + return this; + }, + + expandNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + if (!node.initialized) { + node._initialize(); + } + + node.expand(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + loadBranch: function(node, rows) { + var settings = this.data("treetable").settings, + tree = this.data("treetable").tree; + + // TODO Switch to $.parseHTML + rows = $(rows); + + if (node == null) { // Inserting new root nodes + this.append(rows); + } else { + var lastNode = this.data("treetable").findLastNode(node); + rows.insertAfter(lastNode.row); + } + + this.data("treetable").loadRows(rows); + + // Make sure nodes are properly initialized + rows.filter("tr").each(function() { + tree[$(this).data(settings.nodeIdAttr)].show(); + }); + + if (node != null) { + // Re-render parent to ensure expander icon is shown (#79) + node.render().expand(); + } + + return this; + }, + + move: function(nodeId, destinationId) { + var destination, node; + + node = this.data("treetable").tree[nodeId]; + destination = this.data("treetable").tree[destinationId]; + this.data("treetable").move(node, destination); + + return this; + }, + + node: function(id) { + return this.data("treetable").tree[id]; + }, + + removeNode: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + this.data("treetable").removeNode(node); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + reveal: function(id) { + var node = this.data("treetable").tree[id]; + + if (node) { + node.reveal(); + } else { + throw new Error("Unknown node '" + id + "'"); + } + + return this; + }, + + sortBranch: function(node, columnOrFunction) { + var settings = this.data("treetable").settings, + prepValue, + sortFun; + + columnOrFunction = columnOrFunction || settings.column; + sortFun = columnOrFunction; + + if ($.isNumeric(columnOrFunction)) { + sortFun = function(a, b) { + var extractValue, valA, valB; + + extractValue = function(node) { + var val = node.row.find("td:eq(" + columnOrFunction + ")").text(); + // Ignore trailing/leading whitespace and use uppercase values for + // case insensitive ordering + return $.trim(val).toUpperCase(); + } + + valA = extractValue(a); + valB = extractValue(b); + + if (valA < valB) return -1; + if (valA > valB) return 1; + return 0; + }; + } + + this.data("treetable").sortBranch(node, sortFun); + return this; + }, + + unloadBranch: function(node) { + this.data("treetable").unloadBranch(node); + return this; + } + }; + + $.fn.treetable = function(method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + return $.error("Method " + method + " does not exist on jQuery.treetable"); + } + }; + + // Expose classes to world + this.TreeTable || (this.TreeTable = {}); + this.TreeTable.Node = Node; + this.TreeTable.Tree = Tree; +})(jQuery); diff --git a/web/modules/contrib/token/js/token.js b/web/modules/contrib/token/js/token.js new file mode 100644 index 000000000..0cd7621d1 --- /dev/null +++ b/web/modules/contrib/token/js/token.js @@ -0,0 +1,58 @@ + +(function ($) { + + 'use strict'; + + Drupal.behaviors.tokenTree = { + attach: function (context, settings) { + $('table.token-tree', context).once('token-tree').each(function () { + $(this).treetable({ expandable: true }); + }); + } + }; + + Drupal.behaviors.tokenInsert = { + attach: function (context, settings) { + // Keep track of which textfield was last selected/focused. + $('textarea, input[type="text"]', context).focus(function () { + drupalSettings.tokenFocusedField = this; + }); + + $('.token-click-insert .token-key', context).once('token-click-insert').each(function () { + var newThis = $('' + $(this).html() + '').click(function () { + if (typeof drupalSettings.tokenFocusedField == 'undefined') { + alert(Drupal.t('First click a text field to insert your tokens into.')); + } + else { + var myField = drupalSettings.tokenFocusedField; + var myValue = $(this).text(); + + // IE support. + if (document.selection) { + myField.focus(); + var sel = document.selection.createRange(); + sel.text = myValue; + } + + // MOZILLA/NETSCAPE support. + else if (myField.selectionStart || myField.selectionStart === '0') { + var startPos = myField.selectionStart; + var endPos = myField.selectionEnd; + myField.value = myField.value.substring(0, startPos) + + myValue + + myField.value.substring(endPos, myField.value.length); + } + else { + myField.value += myValue; + } + + $('html,body').animate({scrollTop: $(myField).offset().top}, 500); + } + return false; + }); + $(this).html(newThis); + }); + } + }; + +})(jQuery, drupalSettings); diff --git a/web/modules/contrib/token/src/Controller/TokenAutocompleteController.php b/web/modules/contrib/token/src/Controller/TokenAutocompleteController.php new file mode 100644 index 000000000..a50da70b8 --- /dev/null +++ b/web/modules/contrib/token/src/Controller/TokenAutocompleteController.php @@ -0,0 +1,78 @@ +treeBuilder = $tree_builder; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('token.tree_builder') + ); + } + + /** + * Retrieves suggestions for block category autocompletion. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * @param string $token_type + * The token type. + * @param string $filter + * The autocomplete filter. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * A JSON response containing autocomplete suggestions. + */ + public function autocomplete($token_type, $filter, Request $request) { + $filter = substr($filter, strrpos($filter, '[')); + + $matches = array(); + + if (!Unicode::strlen($filter)) { + $matches["[{$token_type}:"] = 0; + } + else { + $depth = max(1, substr_count($filter, ':')); + $tree = $this->treeBuilder->buildTree($token_type, ['flat' => TRUE, 'depth' => $depth]); + foreach (array_keys($tree) as $token) { + if (strpos($token, $filter) === 0) { + $matches[$token] = levenshtein($token, $filter); + if (isset($tree[$token]['children'])) { + $token = rtrim($token, ':]') . ':'; + $matches[$token] = levenshtein($token, $filter); + } + } + } + } + + asort($matches); + + $keys = array_keys($matches); + $matches = array_combine($keys, $keys); + + return new JsonResponse($matches); + } + +} diff --git a/web/modules/contrib/token/src/Controller/TokenCacheController.php b/web/modules/contrib/token/src/Controller/TokenCacheController.php new file mode 100644 index 000000000..39dc5b0f3 --- /dev/null +++ b/web/modules/contrib/token/src/Controller/TokenCacheController.php @@ -0,0 +1,21 @@ +redirect(''); + } + +} diff --git a/web/modules/contrib/token/src/Controller/TokenDevelController.php b/web/modules/contrib/token/src/Controller/TokenDevelController.php new file mode 100644 index 000000000..332bad251 --- /dev/null +++ b/web/modules/contrib/token/src/Controller/TokenDevelController.php @@ -0,0 +1,109 @@ +treeBuilder = $tree_builder; + $this->entityMapper = $entity_mapper; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('token.tree_builder'), + $container->get('token.entity_mapper') + ); + } + + /** + * Prints the loaded structure of the current entity. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * A RouteMatch object. + * + * @return array + * Array of page elements to render. + */ + public function entityTokens(RouteMatchInterface $route_match) { + $output = []; + + $parameter_name = $route_match->getRouteObject()->getOption('_token_entity_type_id'); + $entity = $route_match->getParameter($parameter_name); + + if ($entity && $entity instanceof EntityInterface) { + $output = $this->renderTokenTree($entity); + } + + return $output; + } + + /** + * Render the token tree for the specified entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which the token tree should be rendered. + * + * @return array + * Render array of the token tree for the $entity. + * + * @see static::entityLoad + */ + protected function renderTokenTree(EntityInterface $entity) { + $this->moduleHandler()->loadInclude('token', 'pages.inc'); + $entity_type = $entity->getEntityTypeId(); + + $token_type = $this->entityMapper->getTokenTypeForEntityType($entity_type); + $options = [ + 'flat' => TRUE, + 'values' => TRUE, + 'data' => [$token_type => $entity], + ]; + + $token_tree = [ + $token_type => [ + 'tokens' => $this->treeBuilder->buildTree($token_type, $options), + ], + ]; +// foreach ($tree as $token => $token_info) { +// if (!isset($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) { +// continue; +// } +// } + + $build['tokens'] = [ + '#type' => 'token_tree_table', + '#show_restricted' => FALSE, + '#show_nested' => FALSE, + '#skip_empty_values' => TRUE, + '#token_tree' => $token_tree, + '#columns' => ['token', 'value'], + '#empty' => $this->t('No tokens available.'), + ]; + + return $build; + } +} diff --git a/web/modules/contrib/token/src/Controller/TokenTreeController.php b/web/modules/contrib/token/src/Controller/TokenTreeController.php new file mode 100644 index 000000000..f233ab234 --- /dev/null +++ b/web/modules/contrib/token/src/Controller/TokenTreeController.php @@ -0,0 +1,56 @@ +treeBuilder = $tree_builder; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('token.tree_builder') + ); + } + + /** + * Page callback to output a token tree as an empty page. + */ + function outputTree(Request $request) { + $options = $request->query->has('options') ? Json::decode($request->query->get('options')) : []; + + // The option token_types may only be an array OR 'all'. If it is not set, + // we assume that only global token types are requested. + $token_types = !empty($options['token_types']) ? $options['token_types'] : []; + if ($token_types == 'all') { + $build = $this->treeBuilder->buildAllRenderable($options); + } + else { + $build = $this->treeBuilder->buildRenderable($token_types, $options); + } + + $build['#cache']['contexts'][] = 'url.query_args:options'; + $build['#title'] = $this->t('Available tokens'); + + return $build; + } + +} diff --git a/web/modules/contrib/token/src/Element/TokenTreeTable.php b/web/modules/contrib/token/src/Element/TokenTreeTable.php new file mode 100644 index 000000000..6de9c95d3 --- /dev/null +++ b/web/modules/contrib/token/src/Element/TokenTreeTable.php @@ -0,0 +1,155 @@ + '-', '_' => '-', '/' => '-', '[' => '-', ']' => '', ':' => '--', '?' => '', '<' => '-', '>' => '-']; + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return [ + '#header' => [], + '#rows' => [], + '#token_tree' => [], + '#columns' => ['name', 'token', 'description'], + '#empty' => '', + '#show_restricted' => FALSE, + '#show_nested' => FALSE, + '#skip_empty_values' => FALSE, + '#click_insert' => TRUE, + '#sticky' => FALSE, + '#responsive' => TRUE, + '#input' => FALSE, + '#pre_render' => [ + [$class, 'preRenderTokenTree'], + [$class, 'preRenderTable'], + ], + '#theme' => 'table__token_tree', + '#attached' => [ + 'library' => [ + 'token/token', + ], + ], + ]; + } + + /** + * Pre-render the token tree to transform rows in the token tree. + * + * @param array $element + * + * @return array + * The processed element. + */ + public static function preRenderTokenTree($element) { + $multiple_token_types = count($element['#token_tree']) > 1; + foreach ($element['#token_tree'] as $token_type => $type_info) { + // Do not show nested tokens. + if (!empty($type_info['nested']) && empty($element['#show_nested'])) { + continue; + } + + if ($multiple_token_types) { + $row = static::formatRow($token_type, $type_info, $element['#columns'], TRUE); + $element['#rows'][] = $row; + } + + foreach ($type_info['tokens'] as $token => $token_info) { + if (!empty($token_info['restricted']) && empty($element['#show_restricted'])) { + continue; + } + if ($element['#skip_empty_values'] && empty($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) { + continue; + } + if ($multiple_token_types && !isset($token_info['parent'])) { + $token_info['parent'] = $token_type; + } + $row = static::formatRow($token, $token_info, $element['#columns']); + $element['#rows'][] = $row; + } + } + + if (!empty($element['#rows'])) { + $element['#attached']['library'][] = 'token/jquery.treeTable'; + } + + // Fill headers if one is not specified. + if (empty($element['#header'])) { + $column_map = [ + 'name' => t('Name'), + 'token' => t('Token'), + 'value' => t('Value'), + 'description' => t('Description'), + ]; + foreach ($element['#columns'] as $col) { + $element['#header'][] = $column_map[$col]; + } + } + + $element['#attributes']['class'][] = 'token-tree'; + + if ($element['#click_insert']) { + $element['#caption'] = t('Click a token to insert it into the field you\'ve last clicked.'); + $element['#attributes']['class'][] = 'token-click-insert'; + } + + return $element; + } + + protected static function cleanCssIdentifier($id) { + return 'token-' . Html::cleanCssIdentifier(trim($id, '[]'), static::$cssFilter); + } + + protected static function formatRow($token, $token_info, $columns, $is_group = FALSE) { + $row = [ + 'id' => static::cleanCssIdentifier($token), + 'data-tt-id' => static::cleanCssIdentifier($token), + 'class' => [], + 'data' => [], + ]; + + foreach ($columns as $col) { + switch ($col) { + case 'name': + $row['data'][$col] = $token_info['name']; + break; + + case 'token': + $row['data'][$col]['data'] = $token; + $row['data'][$col]['class'][] = 'token-key'; + break; + + case 'description': + $row['data'][$col] = isset($token_info['description']) ? $token_info['description'] : ''; + break; + + case 'value': + $row['data'][$col] = !$is_group && isset($token_info['value']) ? $token_info['value'] : ''; + break; + } + } + + if ($is_group) { + // This is a token type/group. + $row['class'][] = 'token-group'; + } + elseif (!empty($token_info['parent'])) { + $row['data-tt-parent-id'] = static::cleanCssIdentifier($token_info['parent']); + unset($row['parent']); + } + + return $row; + } +} diff --git a/web/modules/contrib/token/src/Plugin/Derivative/DevelLocalTask.php b/web/modules/contrib/token/src/Plugin/Derivative/DevelLocalTask.php new file mode 100644 index 000000000..5b22e8440 --- /dev/null +++ b/web/modules/contrib/token/src/Plugin/Derivative/DevelLocalTask.php @@ -0,0 +1,56 @@ +entityTypeManager = $entity_type_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $this->derivatives = []; + + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->hasLinkTemplate('token-devel')) { + $this->derivatives["$entity_type_id.token_devel_tab"] = [ + 'route_name' => "entity.$entity_type_id.token_devel", + 'weight' => 110, + 'title' => $this->t('Tokens'), + 'parent_id' => "devel.entities:$entity_type_id.devel_tab", + ]; + } + } + + foreach ($this->derivatives as &$entry) { + $entry += $base_plugin_definition; + } + + return $this->derivatives; + } +} diff --git a/web/modules/contrib/token/src/Routing/RouteSubscriber.php b/web/modules/contrib/token/src/Routing/RouteSubscriber.php new file mode 100644 index 000000000..ae6815a28 --- /dev/null +++ b/web/modules/contrib/token/src/Routing/RouteSubscriber.php @@ -0,0 +1,74 @@ +entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + protected function alterRoutes(RouteCollection $collection) { + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($devel_render = $entity_type->getLinkTemplate('token-devel')) { + $options = [ + '_admin_route' => TRUE, + '_token_entity_type_id' => $entity_type_id, + 'parameters' => [ + $entity_type_id => [ + 'type' => 'entity:' . $entity_type_id, + ], + ], + ]; + + $route = new Route( + $devel_render, + [ + '_controller' => '\Drupal\token\Controller\TokenDevelController::entityTokens', + '_title' => 'Devel Tokens', + ], + [ + '_permission' => 'access devel information', + '_module_dependencies' => 'devel', + ], + $options + ); + + $collection->add("entity.$entity_type_id.token_devel", $route); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = parent::getSubscribedEvents(); + $events[RoutingEvents::ALTER] = array('onAlterRoutes', 100); + return $events; + } +} diff --git a/web/modules/contrib/token/src/Tests/TokenBlockTest.php b/web/modules/contrib/token/src/Tests/TokenBlockTest.php new file mode 100644 index 000000000..66e30255e --- /dev/null +++ b/web/modules/contrib/token/src/Tests/TokenBlockTest.php @@ -0,0 +1,72 @@ +admin_user = $this->drupalCreateUser(array('access content', 'administer blocks')); + $this->drupalLogin($this->admin_user); + } + + public function testBlockTitleTokens() { + $label = 'tokenblock'; + $bundle = BlockContentType::create(array( + 'id' => $label, + 'label' => $label, + 'revision' => FALSE + )); + $bundle->save(); + + $block_content = BlockContent::create(array( + 'type' => $label, + 'label' => '[current-page:title] block title', + 'info' => 'Test token title block', + 'body[value]' => 'This is the test token title block.', + )); + $block_content->save(); + + $block = $this->drupalPlaceBlock('block_content:' . $block_content->uuid(), array( + 'label' => '[user:name]', + )); + $this->drupalGet($block->urlInfo()); + // Ensure that the link to available tokens is present and correctly + // positioned. + $this->assertLink('Browse available tokens.'); + $this->assertText('This field supports tokens. Browse available tokens.'); + $this->drupalPostForm(NULL, array(), t('Save block')); + // Ensure token validation is working on the block. + $this->assertText('Title is using the following invalid tokens: [user:name].'); + + // Create the block for real now with a valid title. + $settings = $block->get('settings'); + $settings['label'] = '[current-page:title] block title'; + $block->set('settings', $settings); + $block->save(); + + // Ensure that tokens are not double-escaped when output as a block title. + $this->drupalCreateContentType(array('type' => 'page')); + $node = $this->drupalCreateNode(array('title' => "Site's first node")); + $this->drupalGet('node/' . $node->id()); + // The apostraphe should only be escaped once. + $this->assertRaw("Site's first node block title"); + } +} diff --git a/web/modules/contrib/token/src/Tests/TokenCurrentPageTest.php b/web/modules/contrib/token/src/Tests/TokenCurrentPageTest.php new file mode 100644 index 000000000..cd8ac3729 --- /dev/null +++ b/web/modules/contrib/token/src/Tests/TokenCurrentPageTest.php @@ -0,0 +1,66 @@ + t('Log in'), + '[current-page:url]' => Url::fromRoute('user.login', [], array('absolute' => TRUE))->toString(), + '[current-page:url:absolute]' => Url::fromRoute('user.login', [], array('absolute' => TRUE))->toString(), + '[current-page:url:relative]' => Url::fromRoute('user.login')->toString(), + '[current-page:url:path]' => '/user/login', + '[current-page:url:args:value:0]' => 'user', + '[current-page:url:args:value:1]' => 'login', + '[current-page:url:args:value:2]' => NULL, + '[current-page:url:unaliased]' => Url::fromRoute('user.login', [], array('absolute' => TRUE, 'alias' => TRUE))->toString(), + '[current-page:page-number]' => 1, + '[current-page:query:foo]' => NULL, + '[current-page:query:bar]' => NULL, + // Deprecated tokens + '[current-page:arg:0]' => 'user', + '[current-page:arg:1]' => 'login', + '[current-page:arg:2]' => NULL, + ); + $this->assertPageTokens('user/login', $tokens); + + $this->drupalCreateContentType(array('type' => 'page')); + $node = $this->drupalCreateNode(array('title' => 'Node title', 'path' => array('alias' => '/node-alias'))); + $tokens = array( + '[current-page:title]' => 'Node title', + '[current-page:url]' => $node->url('canonical', array('absolute' => TRUE)), + '[current-page:url:absolute]' => $node->url('canonical', array('absolute' => TRUE)), + '[current-page:url:relative]' => $node->url(), + '[current-page:url:alias]' => '/node-alias', + '[current-page:url:args:value:0]' => 'node-alias', + '[current-page:url:args:value:1]' => NULL, + '[current-page:url:unaliased]' => $node->url('canonical', array('absolute' => TRUE, 'alias' => TRUE)), + '[current-page:url:unaliased:args:value:0]' => 'node', + '[current-page:url:unaliased:args:value:1]' => $node->id(), + '[current-page:url:unaliased:args:value:2]' => NULL, + '[current-page:page-number]' => 1, + '[current-page:query:foo]' => 'bar', + '[current-page:query:bar]' => NULL, + // Deprecated tokens + '[current-page:arg:0]' => 'node', + '[current-page:arg:1]' => 1, + '[current-page:arg:2]' => NULL, + ); + $this->assertPageTokens("/node/{$node->id()}", $tokens, array(), array('url_options' => array('query' => array('foo' => 'bar')))); + } +} diff --git a/web/modules/contrib/token/src/Tests/TokenFieldUiTest.php b/web/modules/contrib/token/src/Tests/TokenFieldUiTest.php new file mode 100644 index 000000000..f7983e73b --- /dev/null +++ b/web/modules/contrib/token/src/Tests/TokenFieldUiTest.php @@ -0,0 +1,280 @@ +adminUser = $this->drupalCreateUser(['administer content types', 'administer node fields']); + $this->drupalLogin($this->adminUser); + + $node_type = NodeType::create([ + 'type' => 'article', + 'name' => 'Article', + 'description' => "Use articles for time-sensitive content like news, press releases or blog posts.", + ]); + $node_type->save(); + + entity_create('field_storage_config', array( + 'field_name' => 'field_body', + 'entity_type' => 'node', + 'type' => 'text_with_summary', + ))->save(); + entity_create('field_config', array( + 'field_name' => 'field_body', + 'label' => 'Body', + 'entity_type' => 'node', + 'bundle' => 'article', + ))->save(); + entity_create('field_storage_config', array( + 'field_name' => 'field_image', + 'entity_type' => 'node', + 'type' => 'image', + ))->save(); + entity_create('field_config', array( + 'field_name' => 'field_image', + 'label' => 'Image', + 'entity_type' => 'node', + 'bundle' => 'article', + ))->save(); + entity_create('field_storage_config', array( + 'field_name' => 'field_image_2', + 'entity_type' => 'node', + 'type' => 'image', + ))->save(); + entity_create('field_config', array( + 'field_name' => 'field_image_2', + 'label' => 'Image 2', + 'entity_type' => 'node', + 'bundle' => 'article', + ))->save(); + entity_create('field_storage_config', array( + 'field_name' => 'multivalued_field_image', + 'entity_type' => 'node', + 'type' => 'image', + ))->save(); + entity_create('field_config', array( + 'field_name' => 'multivalued_field_image', + 'label' => 'Multivalued field image', + 'entity_type' => 'node', + 'bundle' => 'article', + ))->save(); + + entity_get_form_display('node', 'article', 'default') + ->setComponent('field_body', [ + 'type' => 'text_textarea_with_summary', + 'settings' => [ + 'rows' => '9', + 'summary_rows' => '3', + ], + 'weight' => 5, + ]) + ->save(); + } + + public function testFileFieldUi() { + $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image'); + + // Ensure the 'Browse available tokens' link is present and correct. + $this->assertLink('Browse available tokens.'); + $this->assertLinkByHref('token/tree'); + + // Ensure that the default file directory value validates correctly. + $this->drupalPostForm(NULL, [], t('Save settings')); + $this->assertText(t('Saved Image configuration.')); + } + + public function testFieldDescriptionTokens() { + $edit = [ + 'description' => 'The site is called [site:name].', + ]; + $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_body', $edit, 'Save settings'); + + $this->drupalGet('node/add/article'); + $this->assertText('The site is called Drupal.'); + } + + /** + * Test that tokens are correctly provided and replaced for the image fields. + */ + public function testImageFieldTokens() { + // Generate 2 different test images. + file_unmanaged_copy(\Drupal::root() . '/core/misc/druplicon.png', 'public://example1.png'); + file_unmanaged_copy(\Drupal::root() . '/core/misc/loading.gif', 'public://example2.gif'); + + // Resize the test images so that they will be scaled down during token + // replacement. + $image1 = \Drupal::service('image.factory')->get('public://example1.png'); + $image1->resize(500, 500); + $image1->save(); + $image2 = \Drupal::service('image.factory')->get('public://example2.gif'); + $image2->resize(500, 500); + $image2->save(); + + /** @var \Drupal\file\Entity\File $image1 */ + $image1 = File::create(['uri' => 'public://example1.png']); + $image1->save(); + /** @var \Drupal\file\Entity\File $image2 */ + $image2 = File::create(['uri' => 'public://example2.gif']); + $image2->save(); + + $node = Node::create([ + 'title' => 'Test node title', + 'type' => 'article', + 'field_image' => [ + [ + 'target_id' => $image1->id(), + ], + ], + 'field_image_2' => [ + [ + 'target_id' => $image2->id(), + ], + ], + 'multivalued_field_image' => [ + ['target_id' => $image1->id()], + ['target_id' => $image2->id()], + ], + ]); + $node->save(); + + // Obtain the file size and dimension of the images that will be scaled + // down during token replacement by applying the styles here. + $style_thumbnail = ImageStyle::load('thumbnail'); + $style_thumbnail->createDerivative('public://example1.png', 'public://styles/thumbnail/public/example1-test.png'); + $style_thumbnail->createDerivative('public://example2.gif', 'public://styles/thumbnail/public/example2-test.gif'); + $image_1_thumbnail = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/example1-test.png'); + $image_2_thumbnail = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/example2-test.gif'); + $style_medium = ImageStyle::load('medium'); + $style_medium->createDerivative('public://example1.png', 'public://styles/medium/public/example1-test.png'); + $style_medium->createDerivative('public://example2.gif', 'public://styles/medium/public/example2-test.gif'); + $image_1_medium = \Drupal::service('image.factory')->get('public://styles/medium/public/example1-test.png'); + $image_2_medium = \Drupal::service('image.factory')->get('public://styles/medium/public/example2-test.gif'); + $style_large = ImageStyle::load('large'); + $style_large->createDerivative('public://example1.png', 'public://styles/large/public/example1-test.png'); + $style_large->createDerivative('public://example2.gif', 'public://styles/large/public/example2-test.gif'); + $image_1_large = \Drupal::service('image.factory')->get('public://styles/large/public/example1-test.png'); + $image_2_large = \Drupal::service('image.factory')->get('public://styles/large/public/example2-test.gif'); + + // Delete the image derivatives, to make sure they are re-created. + unlink('public://styles/thumbnail/public/example1-test.png'); + unlink('public://styles/medium/public/example1-test.png'); + unlink('public://styles/large/public/example1-test.png'); + unlink('public://styles/thumbnail/public/example2-test.gif'); + unlink('public://styles/medium/public/example2-test.gif'); + unlink('public://styles/large/public/example2-test.gif'); + + $tokens = [ + // field_image + 'field_image:thumbnail:mimetype' => 'image/png', + 'field_image:medium:mimetype' => 'image/png', + 'field_image:large:mimetype' => 'image/png', + 'field_image:thumbnail:filesize' => $image_1_thumbnail->getFileSize(), + 'field_image:medium:filesize' => $image_1_medium->getFileSize(), + 'field_image:large:filesize' => $image_1_large->getFileSize(), + 'field_image:thumbnail:height' => '100', + 'field_image:medium:height' => '220', + 'field_image:large:height' => '480', + 'field_image:thumbnail:width' => '100', + 'field_image:medium:width' => '220', + 'field_image:large:width' => '480', + 'field_image:thumbnail:uri' => 'public://styles/thumbnail/public/example1.png', + 'field_image:medium:uri' => 'public://styles/medium/public/example1.png', + 'field_image:large:uri' => 'public://styles/large/public/example1.png', + 'field_image:thumbnail:url' => $style_thumbnail->buildUrl('public://example1.png'), + 'field_image:medium:url' => $style_medium->buildUrl('public://example1.png'), + 'field_image:large:url' => $style_large->buildUrl('public://example1.png'), + 'field_image:thumbnail' => $style_thumbnail->buildUrl('public://example1.png'), + 'field_image:medium' => $style_medium->buildUrl('public://example1.png'), + 'field_image:large' => $style_large->buildUrl('public://example1.png'), + // field_image_2 + 'field_image_2:thumbnail:mimetype' => 'image/gif', + 'field_image_2:medium:mimetype' => 'image/gif', + 'field_image_2:large:mimetype' => 'image/gif', + 'field_image_2:thumbnail:filesize' => $image_2_thumbnail->getFileSize(), + 'field_image_2:medium:filesize' => $image_2_medium->getFileSize(), + 'field_image_2:large:filesize' => $image_2_large->getFileSize(), + 'field_image_2:thumbnail:height' => '100', + 'field_image_2:medium:height' => '220', + 'field_image_2:large:height' => '480', + 'field_image_2:thumbnail:width' => '100', + 'field_image_2:medium:width' => '220', + 'field_image_2:large:width' => '480', + 'field_image_2:thumbnail:uri' => 'public://styles/thumbnail/public/example2.gif', + 'field_image_2:medium:uri' => 'public://styles/medium/public/example2.gif', + 'field_image_2:large:uri' => 'public://styles/large/public/example2.gif', + 'field_image_2:thumbnail:url' => $style_thumbnail->buildUrl('public://example2.gif'), + 'field_image_2:medium:url' => $style_medium->buildUrl('public://example2.gif'), + 'field_image_2:large:url' => $style_large->buildUrl('public://example2.gif'), + 'field_image_2:thumbnail' => $style_thumbnail->buildUrl('public://example2.gif'), + 'field_image_2:medium' => $style_medium->buildUrl('public://example2.gif'), + 'field_image_2:large' => $style_large->buildUrl('public://example2.gif'), + // multivalued_field_image:0, test for thumbnail image style only. + 'multivalued_field_image:0:thumbnail:mimetype' => 'image/png', + 'multivalued_field_image:0:thumbnail:filesize' => $image_1_thumbnail->getFileSize(), + 'multivalued_field_image:0:thumbnail:height' => '100', + 'multivalued_field_image:0:thumbnail:width' => '100', + 'multivalued_field_image:0:thumbnail:uri' => 'public://styles/thumbnail/public/example1.png', + 'multivalued_field_image:0:thumbnail:url' => $style_thumbnail->buildUrl('public://example1.png'), + 'multivalued_field_image:0:thumbnail' => $style_thumbnail->buildUrl('public://example1.png'), + // multivalued_field_image:1, test for medium image style only. + 'multivalued_field_image:1:medium:mimetype' => 'image/gif', + 'multivalued_field_image:1:medium:filesize' => $image_2_medium->getFileSize(), + 'multivalued_field_image:1:medium:height' => '220', + 'multivalued_field_image:1:medium:width' => '220', + 'multivalued_field_image:1:medium:uri' => 'public://styles/medium/public/example2.gif', + 'multivalued_field_image:1:medium:url' => $style_medium->buildUrl('public://example2.gif'), + 'multivalued_field_image:1:medium' => $style_medium->buildUrl('public://example2.gif'), + ]; + $this->assertTokens('node', ['node' => $node], $tokens); + + /** @var \Drupal\token\Token $token_service */ + $token_service = \Drupal::service('token'); + + // Test one of the image style's token info for cardinality 1 image field. + $token_info = $token_service->getTokenInfo('node-field_image', 'thumbnail'); + $this->assertEqual('Thumbnail (100×100)', $token_info['name']); + $this->assertEqual('Represents the image in the given image style.', $token_info['description']); + + // Test one of the image style's token info for a multivalued image field. + $token_info = $token_service->getTokenInfo('node-multivalued_field_image', 'medium'); + $this->assertEqual('Medium (220×220)', $token_info['name']); + $this->assertEqual('Represents the image in the given image style.', $token_info['description']); + + // Test few of the image styles' properties token info. + $token_info = $token_service->getTokenInfo('image_with_image_style', 'mimetype'); + $this->assertEqual('MIME type', $token_info['name']); + $this->assertEqual('The MIME type (image/png, image/bmp, etc.) of the image.', $token_info['description']); + + $token_info = $token_service->getTokenInfo('image_with_image_style', 'filesize'); + $this->assertEqual('File size', $token_info['name']); + $this->assertEqual('The file size of the image.', $token_info['description']); + } + +} diff --git a/web/modules/contrib/token/src/Tests/TokenMenuTest.php b/web/modules/contrib/token/src/Tests/TokenMenuTest.php new file mode 100644 index 000000000..b9eb92061 --- /dev/null +++ b/web/modules/contrib/token/src/Tests/TokenMenuTest.php @@ -0,0 +1,459 @@ +drupalCreateContentType(['type' => 'page']); + // Add a menu. + $menu = entity_create('menu', array( + 'id' => 'main-menu', + 'label' => 'Main menu', + 'description' => 'The Main menu is used on many sites to show the major sections of the site, often in a top navigation bar.', + )); + $menu->save(); + + // Place the menu block. + $this->drupalPlaceBlock('system_menu_block:main-menu'); + + // Add a root link. + /** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $root_link */ + $root_link = entity_create('menu_link_content', array( + 'link' => ['uri' => 'internal:/admin'], + 'title' => 'Administration', + 'menu_name' => 'main-menu', + )); + $root_link->save(); + + // Add another link with the root link as the parent. + /** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $parent_link */ + $parent_link = entity_create('menu_link_content', array( + 'link' => ['uri' => 'internal:/admin/config'], + 'title' => 'Configuration', + 'menu_name' => 'main-menu', + 'parent' => $root_link->getPluginId(), + )); + $parent_link->save(); + + // Test menu link tokens. + $tokens = array( + 'id' => $parent_link->getPluginId(), + 'title' => 'Configuration', + 'menu' => 'Main menu', + 'menu:name' => 'Main menu', + 'menu:machine-name' => $menu->id(), + 'menu:description' => 'The Main menu is used on many sites to show the major sections of the site, often in a top navigation bar.', + 'menu:menu-link-count' => '2', + 'menu:edit-url' => Url::fromRoute('entity.menu.edit_form', ['menu' => 'main-menu'], array('absolute' => TRUE))->toString(), + 'url' => Url::fromRoute('system.admin_config', [], array('absolute' => TRUE))->toString(), + 'url:absolute' => Url::fromRoute('system.admin_config', [], array('absolute' => TRUE))->toString(), + 'url:relative' => Url::fromRoute('system.admin_config', [], array('absolute' => FALSE))->toString(), + 'url:path' => '/admin/config', + 'url:alias' => '/admin/config', + 'edit-url' => Url::fromRoute('entity.menu_link_content.canonical', ['menu_link_content' => $parent_link->id()], array('absolute' => TRUE))->toString(), + 'parent' => 'Administration', + 'parent:id' => $root_link->getPluginId(), + 'parent:title' => 'Administration', + 'parent:menu' => 'Main menu', + 'parent:parent' => NULL, + 'parents' => 'Administration', + 'parents:count' => 1, + 'parents:keys' => $root_link->getPluginId(), + 'root' => 'Administration', + 'root:id' => $root_link->getPluginId(), + 'root:parent' => NULL, + 'root:root' => NULL, + ); + $this->assertTokens('menu-link', array('menu-link' => $parent_link), $tokens); + + // Add a node. + $node = $this->drupalCreateNode(); + + // Allow main menu for this node type. + //$this->config('menu.entity.node.' . $node->getType())->set('available_menus', array('main-menu'))->save(); + + // Add a node menu link. + /** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $node_link */ + $node_link = entity_create('menu_link_content', array( + 'link' => ['uri' =>'entity:node/' . $node->id()], + 'title' => 'Node link', + 'parent' => $parent_link->getPluginId(), + 'menu_name' => 'main-menu', + )); + $node_link->save(); + + // Test [node:menu] tokens. + $tokens = array( + 'menu-link' => 'Node link', + 'menu-link:id' => $node_link->getPluginId(), + 'menu-link:title' => 'Node link', + 'menu-link:menu' => 'Main menu', + 'menu-link:url' => $node->url('canonical', ['absolute' => TRUE]), + 'menu-link:url:path' => '/node/' . $node->id(), + 'menu-link:edit-url' => $node_link->url('edit-form', ['absolute' => TRUE]), + 'menu-link:parent' => 'Configuration', + 'menu-link:parent:id' => $parent_link->getPluginId(), + 'menu-link:parents' => 'Administration, Configuration', + 'menu-link:parents:count' => 2, + 'menu-link:parents:keys' => $root_link->getPluginId() . ', ' . $parent_link->getPluginId(), + 'menu-link:root' => 'Administration', + 'menu-link:root:id' => $root_link->getPluginId(), + ); + $this->assertTokens('node', array('node' => $node), $tokens); + + // Reload the node which will not have $node->menu defined and re-test. + $loaded_node = Node::load($node->id()); + $this->assertTokens('node', array('node' => $loaded_node), $tokens); + + // Regression test for http://drupal.org/node/1317926 to ensure the + // original node object is not changed when calling menu_node_prepare(). + $this->assertTrue(!isset($loaded_node->menu), t('The $node->menu property was not modified during token replacement.'), 'Regression'); + + // Now add a node with a menu-link from the UI and ensure it works. + $this->drupalLogin($this->drupalCreateUser([ + 'create page content', + 'edit any page content', + 'administer menu', + 'administer nodes', + 'administer content types', + 'access administration pages', + ])); + // Setup node type menu options. + $edit = array( + 'menu_options[main-menu]' => 1, + 'menu_options[main]' => 1, + 'menu_parent' => 'main-menu:', + ); + $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); + + // Use a menu-link token in the body. + $this->drupalGet('node/add/page'); + $this->drupalPostForm(NULL, [ + // This should get replaced on save. + // @see token_module_test_node_presave() + 'title[0][value]' => 'Node menu title test', + 'body[0][value]' => 'This is a [node:menu-link:title] token to the menu link title', + 'menu[enabled]' => 1, + 'menu[title]' => 'Test preview', + ], t('Save and publish')); + $node = $this->drupalGetNodeByTitle('Node menu title test'); + $this->assertEqual('This is a Test preview token to the menu link title', $node->body->value); + + // Disable the menu link, save the node and verify that the menu link is + // no longer displayed. + $link = menu_ui_get_menu_link_defaults($node); + $this->drupalPostForm('admin/structure/menu/manage/main-menu', ['links[menu_plugin_id:' . $link['id'] . '][enabled]' => FALSE], t('Save')); + $this->assertText('Menu Main menu has been updated.'); + $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and keep published')); + $this->assertNoLink('Test preview'); + + // Now test a parent link and token. + $this->drupalGet('node/add/page'); + // Make sure that the previous node save didn't result in two menu-links + // being created by the computed menu-link ER field. + // @see token_entity_base_field_info() + // @see token_node_menu_link_submit() + $selects = $this->cssSelect('select[name="menu[menu_parent]"]'); + $select = reset($selects); + $options = $this->getAllOptions($select); + // Filter to items with title containing 'Test preview'. + $options = array_filter($options, function(\SimpleXMLElement $item) { + return strpos((string) $item[0], 'Test preview') !== FALSE; + }); + $this->assertEqual(1, count($options)); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'Node menu title parent path test', + 'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent', + 'menu[enabled]' => 1, + 'menu[title]' => 'Child link', + 'menu[menu_parent]' => 'main-menu:' . $parent_link->getPluginId(), + ], t('Save and publish')); + $node = $this->drupalGetNodeByTitle('Node menu title parent path test'); + $this->assertEqual('This is a /admin/config token to the menu link parent', $node->body->value); + + // Now edit the node and update the parent and title. + $this->drupalPostForm('node/' . $node->id() . '/edit', [ + 'menu[menu_parent]' => 'main-menu:' . $node_link->getPluginId(), + 'title[0][value]' => 'Node menu title edit parent path test', + 'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent', + ], t('Save and keep published')); + $node = $this->drupalGetNodeByTitle('Node menu title edit parent path test', TRUE); + $this->assertEqual(sprintf('This is a /node/%d token to the menu link parent', $loaded_node->id()), $node->body->value); + + // Make sure that the previous node edit didn't result in two menu-links + // being created by the computed menu-link ER field. + // @see token_entity_base_field_info() + // @see token_node_menu_link_submit() + $this->drupalGet('node/add/page'); + $selects = $this->cssSelect('select[name="menu[menu_parent]"]'); + $select = reset($selects); + $options = $this->getAllOptions($select); + // Filter to items with title containing 'Test preview'. + $options = array_filter($options, function(\SimpleXMLElement $item) { + return strpos((string) $item[0], 'Child link') !== FALSE; + }); + $this->assertEqual(1, count($options)); + + // Now add a new node with no menu. + $this->drupalGet('node/add/page'); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'Node menu adding menu later test', + 'body[0][value]' => 'Going to add a menu link on edit', + 'menu[enabled]' => 0, + ], t('Save and publish')); + $node = $this->drupalGetNodeByTitle('Node menu adding menu later test'); + // Now edit it and add a menu item. + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->drupalPostForm(NULL, [ + 'title[0][value]' => 'Node menu adding menu later test', + 'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent', + 'menu[enabled]' => 1, + 'menu[title]' => 'Child link', + 'menu[menu_parent]' => 'main-menu:' . $parent_link->getPluginId(), + ], t('Save and keep published')); + $node = $this->drupalGetNodeByTitle('Node menu adding menu later test', TRUE); + $this->assertEqual('This is a /admin/config token to the menu link parent', $node->body->value); + // And make sure the menu link exists with the right URI. + $link = menu_ui_get_menu_link_defaults($node); + $this->assertTrue(!empty($link['entity_id'])); + $query = \Drupal::entityQuery('menu_link_content') + ->condition('link.uri', 'entity:node/' . $node->id()) + ->sort('id', 'ASC') + ->range(0, 1); + $result = $query->execute(); + $this->assertTrue($result); + + // Create a node with a menu link and create 2 menu links linking to this + // node after. Verify that the menu link provided by the node has priority. + $node_title = $this->randomMachineName(); + $edit = [ + 'title[0][value]' => $node_title, + 'menu[enabled]' => 1, + 'menu[title]' => 'menu link provided by node', + ]; + $this->drupalPostForm('node/add/page', $edit, t('Save and publish')); + $this->assertText('page ' . $node_title . ' has been created'); + $node = $this->drupalGetNodeByTitle($node_title); + + $menu_ui_link1 = entity_create('menu_link_content', [ + 'link' => ['uri' => 'entity:node/' . $node->id()], + 'title' => 'menu link 1 provided by menu ui', + 'menu_name' => 'main-menu', + ]); + $menu_ui_link1->save(); + + $menu_ui_link2 = entity_create('menu_link_content', [ + 'link' => ['uri' => 'entity:node/' . $node->id()], + 'title' => 'menu link 2 provided by menu ui', + 'menu_name' => 'main-menu', + ]); + $menu_ui_link2->save(); + + $tokens = [ + 'menu-link' => 'menu link provided by node', + 'menu-link:title' => 'menu link provided by node', + ]; + $this->assertTokens('node', ['node' => $node], $tokens); + } + + /** + * Tests that the module doesn't affect integrity of the menu, when + * translating them and that menu links tokens are correct. + */ + function testMultilingualMenu() { + // Place the menu block. + $this->drupalPlaceBlock('system_menu_block:main'); + + // Add a second language. + $language = ConfigurableLanguage::create([ + 'id' => 'de', + 'label' => 'German', + ]); + $language->save(); + + // Create the article content type. + $node_type = NodeType::create([ + 'type' => 'article', + ]); + $node_type->save(); + + $permissions = array( + 'access administration pages', + 'administer content translation', + 'administer content types', + 'administer languages', + 'create content translations', + 'create article content', + 'edit any article content', + 'translate any entity', + 'administer menu', + ); + $this->drupalLogin($this->drupalCreateUser($permissions)); + + // Enable translation for articles and menu links. + $this->drupalGet('admin/config/regional/content-language'); + $edit = array( + 'entity_types[node]' => TRUE, + 'entity_types[menu_link_content]' => TRUE, + 'settings[node][article][translatable]' => TRUE, + 'settings[node][article][fields][title]' => TRUE, + 'settings[menu_link_content][menu_link_content][translatable]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, t('Save configuration')); + $this->assertText('Settings successfully updated.'); + + // Create an english node with an english menu. + $this->drupalGet('/node/add/article'); + $edit = [ + 'title[0][value]' => 'English test node with menu', + 'menu[enabled]' => TRUE, + 'menu[title]' => 'English menu title', + ]; + $this->drupalPostForm('/node/add/article', $edit, t('Save')); + $this->assertText('English test node with menu has been created.'); + + // Add a german translation. + $this->drupalGet('node/1/translations'); + $this->clickLink('Add'); + $edit = [ + 'title[0][value]' => 'German test node with menu', + 'menu[enabled]' => TRUE, + 'menu[title]' => 'German menu title', + ]; + $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); + $this->assertText('German test node with menu has been updated.'); + + // Verify that the menu links are correct. + $this->drupalGet('node/1'); + $this->assertLink('English menu title'); + $this->drupalGet('de/node/1'); + $this->assertLink('German menu title'); + + // Verify that tokens are correct. + $node = Node::load(1); + $this->assertTokens('node', ['node' => $node], ['menu-link' => 'English menu title']); + $this->assertTokens('node', ['node' => $node], [ + 'menu-link' => 'German menu title', + 'menu-link:title' => 'German menu title', + ], ['langcode' => 'de']); + + // Get the menu link and create a child menu link to assert parent and root + // tokens. + $url = $node->toUrl(); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters()); + $link = reset($links); + + $base_options = [ + 'provider' => 'menu_test', + 'menu_name' => 'menu_test', + ]; + $child_1 = $base_options + [ + 'title' => 'child_1 title EN', + 'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1'], + 'parent' => $link->getPluginId(), + 'langcode' => 'en', + ]; + $child_1 = MenuLinkContent::create($child_1); + $child_1->save(); + + // Add the german translation. + $child_1->addTranslation('de', ['title' => 'child_1 title DE'] + $child_1->toArray()); + $child_1->save(); + + $this->assertTokens('menu-link', ['menu-link' => $child_1], [ + 'title' => 'child_1 title EN', + 'parents' => 'English menu title', + 'root' => 'English menu title', + ]); + $this->assertTokens('menu-link', ['menu-link' => $child_1], [ + 'title' => 'child_1 title DE', + 'parents' => 'German menu title', + 'root' => 'German menu title', + ], ['langcode' => 'de']); + } + + /** + * Tests menu link parents token. + */ + public function testMenuLinkParentsToken() { + // Create a menu with a simple link hierarchy : + // - parent + // - child-1 + // - child-1-1 + Menu::create(array( + 'id' => 'menu_test', + 'label' => 'Test menu', + ))->save(); + $base_options = [ + 'provider' => 'menu_test', + 'menu_name' => 'menu_test', + ]; + $parent = $base_options + [ + 'title' => 'parent title', + 'link' => ['uri' => 'internal:/menu-test/hierarchy/parent'], + ]; + $parent = MenuLinkContent::create($parent); + $parent->save(); + $child_1 = $base_options + [ + 'title' => 'child_1 title', + 'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1'], + 'parent' => $parent->getPluginId(), + ]; + $child_1 = MenuLinkContent::create($child_1); + $child_1->save(); + $child_1_1 = $base_options + [ + 'title' => 'child_1_1 title', + 'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1/child_1_1'], + 'parent' => $child_1->getPluginId(), + ]; + $child_1_1 = MenuLinkContent::create($child_1_1); + $child_1_1->save(); + + $this->assertTokens('menu-link', ['menu-link' => $child_1_1], ['parents' => 'parent title, child_1 title']); + + // Change the parent of child_1_1 to 'parent' at the entity level. + $child_1_1->parent->value = $parent->getPluginId(); + $child_1_1->save(); + + $this->assertTokens('menu-link', ['menu-link' => $child_1_1], ['parents' => 'parent title']); + + // Change the parent of child_1_1 to 'main', at the entity level. + $child_1_1->parent->value = ''; + $child_1_1->save(); + + // The token shouldn't have been generated; the menu link has no parent. + $this->assertNoTokens('menu-link', ['menu-link' => $child_1_1], ['parents']); + } + +} diff --git a/web/modules/contrib/token/src/Tests/TokenTestBase.php b/web/modules/contrib/token/src/Tests/TokenTestBase.php new file mode 100644 index 000000000..29c1ac3cd --- /dev/null +++ b/web/modules/contrib/token/src/Tests/TokenTestBase.php @@ -0,0 +1,21 @@ +assertTokens($type, $data, array($token => $expected), $options); + } + + function assertTokens($type, array $data, array $tokens, array $options = array()) { + $input = $this->mapTokenNames($type, array_keys($tokens)); + $bubbleable_metadata = new BubbleableMetadata(); + $replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata); + foreach ($tokens as $name => $expected) { + $token = $input[$name]; + if (!isset($expected)) { + $this->assertTrue(!isset($replacements[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); + } + elseif (!isset($replacements[$token])) { + $this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); + } + elseif (!empty($options['regex'])) { + $this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); + } + else { + $this->assertEqual($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); + } + } + + return $replacements; + } + + function mapTokenNames($type, array $tokens = array()) { + $return = array(); + foreach ($tokens as $token) { + $return[$token] = "[$type:$token]"; + } + return $return; + } + + function assertNoTokens($type, array $data, array $tokens, array $options = array()) { + $input = $this->mapTokenNames($type, $tokens); + $bubbleable_metadata = new BubbleableMetadata(); + $replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata); + foreach ($tokens as $name) { + $token = $input[$name]; + $this->assertTrue(!isset($replacements[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); + } + } + + function saveAlias($source, $alias, $language = Language::LANGCODE_NOT_SPECIFIED) { + $alias = array( + 'source' => $source, + 'alias' => $alias, + 'language' => $language, + ); + \Drupal::service('path.alias_storage')->save($alias['source'], $alias['alias']); + return $alias; + } + + function saveEntityAlias($entity_type, EntityInterface $entity, $alias, $language = Language::LANGCODE_NOT_SPECIFIED) { + $uri = $entity->toUrl()->toArray(); + return $this->saveAlias($uri['path'], $alias, $language); + } + + /** + * Make a page request and test for token generation. + */ + function assertPageTokens($url, array $tokens, array $data = array(), array $options = array()) { + if (empty($tokens)) { + return TRUE; + } + + $token_page_tokens = array( + 'tokens' => $tokens, + 'data' => $data, + 'options' => $options, + ); + \Drupal::state()->set('token_page_tokens', $token_page_tokens); + + $options += array('url_options' => array()); + $this->drupalGet($url, $options['url_options']); + $this->refreshVariables(); + $result = \Drupal::state()->get('token_page_tokens', array()); + + if (!isset($result['values']) || !is_array($result['values'])) { + return $this->fail('Failed to generate tokens.'); + } + + foreach ($tokens as $token => $expected) { + if (!isset($expected)) { + $this->assertTrue(!isset($result['values'][$token]) || $result['values'][$token] === $token, t("Token value for @token was not generated.", array('@token' => $token))); + } + elseif (!isset($result['values'][$token])) { + $this->fail(t('Failed to generate token @token.', array('@token' => $token))); + } + else { + $this->assertIdentical($result['values'][$token], (string) $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@token' => $token, '@actual' => $result['values'][$token], '@expected' => $expected))); + } + } + } + +} diff --git a/web/modules/contrib/token/src/Tests/TokenURLTest.php b/web/modules/contrib/token/src/Tests/TokenURLTest.php new file mode 100644 index 000000000..6241f3b69 --- /dev/null +++ b/web/modules/contrib/token/src/Tests/TokenURLTest.php @@ -0,0 +1,51 @@ +saveAlias('/node/1', '/first-node'); + } + + function testURLTokens() { + $url = new Url('entity.node.canonical', array('node' => 1)); + $tokens = array( + 'absolute' => $url->setAbsolute()->toString(), + 'relative' => $url->setAbsolute(FALSE)->toString(), + 'path' => '/first-node', + 'brief' => preg_replace(array('!^https?://!', '!/$!'), '', $url->setAbsolute()->toString()), + 'args:value:0' => 'first-node', + 'args:value:1' => NULL, + 'args:value:N' => NULL, + 'unaliased' => $url->setAbsolute()->setOption('alias', TRUE)->toString(), + 'unaliased:relative' => $url->setAbsolute(FALSE)->setOption('alias', TRUE)->toString(), + 'unaliased:path' => '/node/1', + 'unaliased:brief' => preg_replace(array('!^https?://!', '!/$!'), '', $url->setAbsolute()->setOption('alias', TRUE)->toString()), + 'unaliased:args:value:0' => 'node', + 'unaliased:args:value:1' => '1', + 'unaliased:args:value:2' => NULL, + // Deprecated tokens. + 'alias' => '/first-node', + ); + $this->assertTokens('url', array('url' => new Url('entity.node.canonical', array('node' => 1))), $tokens); + } +} diff --git a/web/modules/contrib/token/src/Tests/TokenUserTest.php b/web/modules/contrib/token/src/Tests/TokenUserTest.php new file mode 100644 index 000000000..2178c7e64 --- /dev/null +++ b/web/modules/contrib/token/src/Tests/TokenUserTest.php @@ -0,0 +1,116 @@ +account = $this->drupalCreateUser(['administer users', 'administer account settings']); + $this->drupalLogin($this->account); + } + + public function testUserTokens() { + // Enable user pictures. + \Drupal::state()->set('user_pictures', 1); + \Drupal::state()->set('user_picture_file_size', ''); + + // Set up the pictures directory. + $picture_path = file_default_scheme() . '://' . \Drupal::state()->get('user_picture_path', 'pictures'); + if (!file_prepare_directory($picture_path, FILE_CREATE_DIRECTORY)) { + $this->fail('Could not create directory ' . $picture_path . '.'); + } + + // Add a user picture to the account. + $image = current($this->drupalGetTestFiles('image')); + $edit = array('files[user_picture_0]' => drupal_realpath($image->uri)); + $this->drupalPostForm('user/' . $this->account->id() . '/edit', $edit, t('Save')); + + $storage = \Drupal::entityTypeManager()->getStorage('user'); + + // Load actual user data from database. + $storage->resetCache(); + $this->account = $storage->load($this->account->id()); + $this->assertTrue(!empty($this->account->user_picture->target_id), 'User picture uploaded.'); + + $picture = [ + '#theme' => 'user_picture', + '#account' => $this->account, + ]; + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $user_tokens = array( + 'picture' => $renderer->renderPlain($picture), + 'picture:fid' => $this->account->user_picture->target_id, + 'picture:size-raw' => 125, + 'ip-address' => NULL, + 'roles' => implode(', ', $this->account->getRoles()), + ); + $this->assertTokens('user', array('user' => $this->account), $user_tokens); + + // Remove the simpletest-created user role. + $roles = $this->account->getRoles(); + $this->account->removeRole(end($roles)); + $this->account->save(); + + // Remove the user picture field and reload the user. + FieldStorageConfig::loadByName('user', 'user_picture')->delete(); + $storage->resetCache(); + $this->account = $storage->load($this->account->id()); + + $user_tokens = array( + 'picture' => NULL, + 'picture:fid' => NULL, + 'ip-address' => NULL, + 'roles' => 'authenticated', + 'roles:keys' => (string) DRUPAL_AUTHENTICATED_RID, + ); + $this->assertTokens('user', array('user' => $this->account), $user_tokens); + + // The ip address token should work for the current user token type. + $tokens = array( + 'ip-address' => \Drupal::request()->getClientIp(), + ); + $this->assertTokens('current-user', array(), $tokens); + + $anonymous = new AnonymousUserSession(); + $tokens = array( + 'roles' => 'anonymous', + 'roles:keys' => (string) DRUPAL_ANONYMOUS_RID, + ); + $this->assertTokens('user', array('user' => $anonymous), $tokens); + } + + public function testUserAccountSettings() { + $this->drupalGet('admin/config/people/accounts'); + $this->assertText('The list of available tokens that can be used in e-mails is provided below.'); + $this->assertLink('Browse available tokens.'); + $this->assertLinkByHref('token/tree'); + } +} \ No newline at end of file diff --git a/web/modules/contrib/token/src/Tests/Tree/AutocompleteTest.php b/web/modules/contrib/token/src/Tests/Tree/AutocompleteTest.php new file mode 100644 index 000000000..30b3ea416 --- /dev/null +++ b/web/modules/contrib/token/src/Tests/Tree/AutocompleteTest.php @@ -0,0 +1,61 @@ +drupalGetJSON($url); + + $this->assertTrue(isset($response['[node:nid]'])); + $this->assertTrue(isset($response['[node:author]'])); + $this->assertTrue(isset($response['[node:url]'])); + $this->assertTrue(isset($response['[node:url:'])); + + $url = $url_prefix . 'Title of [node:url:'; + $response = $this->drupalGetJSON($url); + + $this->assertTrue(isset($response['[node:url:path]'])); + $this->assertTrue(isset($response['[node:url:absolute]'])); + } + + /** + * Tests autocomplete for user tokens. + */ + public function testUserAutocomplete() { + $url_prefix = "token/autocomplete/user/"; + + $url = $url_prefix . 'Name of the [us'; + $response = $this->drupalGetJSON($url); + + $this->assertTrue(isset($response['[user:uid]'])); + $this->assertTrue(isset($response['[user:original]'])); + $this->assertTrue(isset($response['[user:url]'])); + $this->assertTrue(isset($response['[user:url:'])); + + $url = $url_prefix . 'Title of [user:original:'; + $response = $this->drupalGetJSON($url); + + $this->assertTrue(isset($response['[user:original:uid]'])); + } +} diff --git a/web/modules/contrib/token/src/Tests/Tree/HelpPageTest.php b/web/modules/contrib/token/src/Tests/Tree/HelpPageTest.php new file mode 100644 index 000000000..a957e3235 --- /dev/null +++ b/web/modules/contrib/token/src/Tests/Tree/HelpPageTest.php @@ -0,0 +1,69 @@ +account = $this->drupalCreateUser(['access administration pages']); + $this->drupalLogin($this->account); + } + + /** + * Tests the token browser on the token help page. + */ + public function testHelpPageTree() { + $this->drupalGet('admin/help/token'); + $this->assertText('The list of the currently available tokens on this site are shown below.'); + + $this->assertTokenGroup('Current date'); + $this->assertTokenGroup('Site information'); + + $this->assertTokenInTree('[current-date:html_date]', 'current-date'); + $this->assertTokenInTree('[current-date:html_week]', 'current-date'); + $this->assertTokenInTree('[date:html_date]', 'date'); + $this->assertTokenInTree('[date:html_week]', 'date'); + + $this->assertTokenInTree('[current-user:account-name]', 'current-user'); + $this->assertTokenInTree('[user:account-name]', 'user'); + + $this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url'); + $this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased'); + $this->assertTokenInTree('[user:original:account-name]', 'user--original'); + + // Assert some of the restricted tokens to ensure they are shown. + $this->assertTokenInTree('[user:one-time-login-url]', 'user'); + $this->assertTokenInTree('[user:original:cancel-url]', 'user--original'); + + // The Array token is marked as nested, so it should not show up as a top + // level token, only nested under another token. For instance, user:roles + // is of type Array and tokens of type Array have 'nested' setting true. + $this->assertTokenNotGroup('Array'); + $this->assertTokenNotGroup('user:roles'); + $this->assertTokenInTree('[user:roles]', 'user'); + } + +} diff --git a/web/modules/contrib/token/src/Tests/Tree/TokenTreeTestTrait.php b/web/modules/contrib/token/src/Tests/Tree/TokenTreeTestTrait.php new file mode 100644 index 000000000..dc70a23b8 --- /dev/null +++ b/web/modules/contrib/token/src/Tests/Tree/TokenTreeTestTrait.php @@ -0,0 +1,131 @@ +xpath('//tr[contains(@class, "token-group")]/td[1]'); + return array_map(function ($item) { + return (string) $item; + }, $groups); + } + + /** + * Check to see if the specified token group is present in the token browser. + * + * @param string $token_group + * The name of the token group. + * @param string $message + * (optional) A message to display with the assertion. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. + */ + protected function assertTokenGroup($token_group, $message = '', $group = 'Other') { + $groups = $this->getTokenGroups(); + + if (!$message) { + $message = "Token group $token_group found."; + } + + $this->assertTrue(in_array($token_group, $groups), $message, $group); + } + + /** + * Check to see if the specified token group is not present in the token + * browser. + * + * @param string $token_group + * The name of the token group. + * @param string $message + * (optional) A message to display with the assertion. + * @param string $group + * (optional) The group this message is not in, which is displayed in a + * column in test output. + */ + protected function assertTokenNotGroup($token_group, $message = '', $group = 'Other') { + $groups = $this->getTokenGroups(); + + if (!$message) { + $message = "Token group $token_group not found."; + } + + $this->assertFalse(in_array($token_group, $groups), $message, $group); + } + + /** + * Check to see if the specified token is present in the token browser. + * + * @param $token + * The token name with the surrounding square brackets []. + * @param string $parent + * (optional) The parent CSS identifier of this token. + * @param string $message + * (optional) A message to display with the assertion. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. + */ + protected function assertTokenInTree($token, $parent = '', $message = '', $group = 'Other') { + $xpath = $this->getXpathForTokenInTree($token, $parent); + + if (!$message) { + $message = "Token $token found."; + } + + $this->assertIdentical(1, count($this->xpath($xpath)), $message, $group); + } + + /** + * Check to see if the specified token is present in the token browser. + * + * @param $token + * The token name with the surrounding square brackets []. + * @param string $parent + * (optional) The parent CSS identifier of this token. + * @param string $message + * (optional) A message to display with the assertion. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. + */ + protected function assertTokenNotInTree($token, $parent = '', $message = '', $group = 'Other') { + $xpath = $this->getXpathForTokenInTree($token, $parent); + + if (!$message) { + $message = "Token $token not found."; + } + + $this->assertIdentical(0, count($this->xpath($xpath)), $message, $group); + } + + /** + * Get xpath to check for token in tree. + * + * @param $token + * The token name with the surrounding square brackets []. + * @param string $parent + * (optional) The parent CSS identifier of this token. + * + * @return string + * The xpath to check for the token and parent. + */ + protected function getXpathForTokenInTree($token, $parent = '') { + $xpath = "//tr"; + if ($parent) { + $xpath .= '[@data-tt-parent-id="token-' . $parent . '"]'; + } + $xpath .= '/td[contains(@class, "token-key") and text() = "' . $token . '"]'; + return $xpath; + } +} diff --git a/web/modules/contrib/token/src/Tests/Tree/TreeTest.php b/web/modules/contrib/token/src/Tests/Tree/TreeTest.php new file mode 100644 index 000000000..ed1dd3cad --- /dev/null +++ b/web/modules/contrib/token/src/Tests/Tree/TreeTest.php @@ -0,0 +1,148 @@ +account = $this->drupalCreateUser(['administer account settings']); + $this->drupalLogin($this->account); + } + + /** + * Test various tokens that are possible on the site. + */ + public function testAllTokens() { + $this->drupalGet($this->getTokenTreeUrl(['token_types' => 'all'])); + + $this->assertTokenGroup('Current date'); + $this->assertTokenGroup('Site information'); + + $this->assertTokenInTree('[current-date:html_date]', 'current-date'); + $this->assertTokenInTree('[current-date:html_week]', 'current-date'); + $this->assertTokenInTree('[date:html_date]', 'date'); + $this->assertTokenInTree('[date:html_week]', 'date'); + + $this->assertTokenInTree('[current-user:account-name]', 'current-user'); + $this->assertTokenInTree('[user:account-name]', 'user'); + + $this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url'); + $this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased'); + $this->assertTokenInTree('[user:original:account-name]', 'user--original'); + } + + /** + * Test various tokens that are possible on the site. + */ + public function testGlobalTokens() { + $this->drupalGet($this->getTokenTreeUrl()); + + $this->assertTokenGroup('Current date'); + $this->assertTokenGroup('Site information'); + + // Assert that non-global tokens are not listed. + $this->assertTokenNotInTree('[user:account-name]', 'user'); + $this->assertTokenNotInTree('[user:original:account-name]', 'user--original'); + + // Assert some of the global tokens, just to be sure. + $this->assertTokenInTree('[current-date:html_date]', 'current-date'); + $this->assertTokenInTree('[current-date:html_week]', 'current-date'); + + $this->assertTokenInTree('[current-user:account-name]', 'current-user'); + + $this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url'); + $this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased'); + } + + /** + * Tests if the token browser displays the user tokens. + */ + public function testUserTokens() { + $this->drupalGet($this->getTokenTreeUrl(['token_types' => ['user']])); + + $this->assertTokenGroup('Users'); + + $this->assertTokenInTree('[user:account-name]', 'user'); + $this->assertTokenInTree('[user:original:account-name]', 'user--original'); + + // Assert some of the restricted tokens to ensure they are not shown. + $this->assertTokenNotInTree('[user:one-time-login-url]', 'user'); + $this->assertTokenNotInTree('[user:original:cancel-url]', 'user--original'); + + // Request with show_restricted set to TRUE to show restricted tokens and + // check for them. + $this->drupalGet($this->getTokenTreeUrl(['token_types' => ['user'], 'show_restricted' => TRUE])); + $this->assertEqual('MISS', $this->drupalGetHeader('x-drupal-dynamic-cache'), 'Cache was not hit'); + $this->assertTokenInTree('[user:one-time-login-url]', 'user'); + $this->assertTokenInTree('[user:original:cancel-url]', 'user--original'); + } + + /** + * Tests if the token browser displays the node tokens. + */ + public function testNodeTokens() { + $this->drupalGet($this->getTokenTreeUrl(['token_types' => ['node']])); + + $this->assertTokenGroup('Nodes'); + + $this->assertTokenInTree('[node:body]', 'node'); + $this->assertTokenInTree('[node:author:original:account-name]', 'node--author--original'); + } + + /** + * Get the URL for the token tree based on the specified options. + * + * The token tree route's URL requires CSRF and cannot be generated in the + * test code. The CSRF token generated using the test runner's session is + * different from the session inside the test environment. This is why the + * link has to be generated inside the environment. + * + * This function calls a page in token_module_test module which generates the + * link and the token. This then replaces the options query parameter with the + * specified options. + * + * The page also uses a title callback to set title to a render array, which + * allows us to test if [current-page:title] works properly. + * + * @param array $options + * The options for the token tree browser. + * + * @return string + * The complete URL of the token tree browser with the CSRF token. + */ + protected function getTokenTreeUrl($options = []) { + $this->drupalGet('token_module_test/browse'); + $this->assertTitle('Available Tokens | Drupal'); + $links = $this->xpath('//a[contains(@href, :href)]/@href', array(':href' => 'token/tree')); + $link = $this->getAbsoluteUrl((string) current($links)); + if (!empty($options)) { + $options = Json::encode($options); + $link = str_replace('options=%5B%5D', 'options=' . urlencode($options), $link); + } + return $link; + } +} diff --git a/web/modules/contrib/token/src/Token.php b/web/modules/contrib/token/src/Token.php new file mode 100644 index 000000000..360ae351c --- /dev/null +++ b/web/modules/contrib/token/src/Token.php @@ -0,0 +1,213 @@ +tokenInfo)) { + $cache_id = 'token_info_sorted:' . $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + $cache = $this->cache->get($cache_id); + if ($cache) { + $this->tokenInfo = $cache->data; + } + else { + $token_info = $this->moduleHandler->invokeAll('token_info'); + $this->moduleHandler->alter('token_info', $token_info); + + foreach (array_keys($token_info['types']) as $type_key) { + if (isset($token_info['types'][$type_key]['type'])) { + $base_type = $token_info['types'][$type_key]['type']; + // If this token type extends another token type, then merge in + // the base token type's tokens. + if (isset($token_info['tokens'][$base_type])) { + $token_info['tokens'] += [$type_key => []]; + $token_info['tokens'][$type_key] += $token_info['tokens'][$base_type]; + } + } + else { + // Add a 'type' value to each token type information. + $token_info['types'][$type_key]['type'] = $type_key; + } + } + + // Pre-sort tokens. + $by_name = $this->prepareMultisort($token_info['types']); + array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['types']); + foreach (array_keys($token_info['tokens']) as $type) { + $by_name = $this->prepareMultisort($token_info['tokens'][$type]); + array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['tokens'][$type]); + } + + $this->tokenInfo = $token_info; + $this->cache->set($cache_id, $this->tokenInfo, CacheBackendInterface::CACHE_PERMANENT, array( + static::TOKEN_INFO_CACHE_TAG, + )); + } + } + + return $this->tokenInfo; + } + + /** + * Extracts data from the token data for use in array_multisort(). + * + * @param array $token_info + * List of tokens or token types, each element must have a name key. + * + * @return string[] + * List of the names keyed by the token key. + */ + protected function prepareMultisort($token_info) { + $by_name = []; + foreach ($token_info as $key => $token_info_element) { + $by_name[$key] = $token_info_element['name']; + } + return $by_name; + } + + /** + * {@inheritdoc} + */ + public function getTokenInfo($token_type, $token) { + if (empty($this->tokenInfo)) { + $this->getInfo(); + } + + return isset($this->tokenInfo['tokens'][$token_type][$token]) ? $this->tokenInfo['tokens'][$token_type][$token] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getTypeInfo($token_type) { + if (empty($this->tokenInfo)) { + $this->getInfo(); + } + + return isset($this->tokenInfo['types'][$token_type]) ? $this->tokenInfo['types'][$token_type] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getGlobalTokenTypes() { + if (empty($this->globalTokenTypes)) { + $token_info = $this->getInfo(); + foreach ($token_info['types'] as $type => $type_info) { + // If the token types has not specified that 'needs-data' => TRUE, then + // it is a global token type that will always be replaced in any context. + if (empty($type_info['needs-data'])) { + $this->globalTokenTypes[] = $type; + } + } + } + + return $this->globalTokenTypes; + } + + /** + * {@inheritdoc} + */ + function getInvalidTokens($type, $tokens) { + $token_info = $this->getInfo(); + $invalid_tokens = array(); + + foreach ($tokens as $token => $full_token) { + if (isset($token_info['tokens'][$type][$token])) { + continue; + } + + // Split token up if it has chains. + $parts = explode(':', $token, 2); + + if (!isset($token_info['tokens'][$type][$parts[0]])) { + // This is an invalid token (not defined). + $invalid_tokens[] = $full_token; + } + elseif (count($parts) == 2) { + $sub_token_info = $token_info['tokens'][$type][$parts[0]]; + if (!empty($sub_token_info['dynamic'])) { + // If this token has been flagged as a dynamic token, skip it. + continue; + } + elseif (empty($sub_token_info['type'])) { + // If the token has chains, but does not support it, it is invalid. + $invalid_tokens[] = $full_token; + } + else { + // Recursively check the chained tokens. + $sub_tokens = $this->findWithPrefix(array($token => $full_token), $parts[0]); + $invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($sub_token_info['type'], $sub_tokens)); + } + } + } + + return $invalid_tokens; + } + + /** + * {@inheritdoc} + */ + public function getInvalidTokensByContext($value, array $valid_types = []) { + if (in_array('all', $valid_types)) { + $info = $this->getInfo(); + $valid_types = array_keys($info['types']); + } + else { + // Add the token types that are always valid in global context. + $valid_types = array_merge($valid_types, $this->getGlobalTokenTypes()); + } + + $invalid_tokens = array(); + $value_tokens = is_string($value) ? $this->scan($value) : $value; + + foreach ($value_tokens as $type => $tokens) { + if (!in_array($type, $valid_types)) { + // If the token type is not a valid context, its tokens are invalid. + $invalid_tokens = array_merge($invalid_tokens, array_values($tokens)); + } + else { + // Check each individual token for validity. + $invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($type, $tokens)); + } + } + + array_unique($invalid_tokens); + return $invalid_tokens; + } + + /** + * {@inheritdoc} + */ + public function resetInfo() { + parent::resetInfo(); + $this->globalTokenTypes = NULL; + } + +} diff --git a/web/modules/contrib/token/src/TokenEntityMapper.php b/web/modules/contrib/token/src/TokenEntityMapper.php new file mode 100644 index 000000000..051df8f6a --- /dev/null +++ b/web/modules/contrib/token/src/TokenEntityMapper.php @@ -0,0 +1,82 @@ +entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getEntityTypeMappings() { + if (empty($this->entityMappings)) { + foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $info) { + $this->entityMappings[$entity_type] = $info->get('token_type') ?: $entity_type; + } + // Allow modules to alter the mapping array. + $this->moduleHandler->alter('token_entity_mapping', $this->entityMappings); + } + + return $this->entityMappings; + } + + /** + * {@inheritdoc} + */ + function getEntityTypeForTokenType($token_type, $fallback = FALSE) { + if (empty($this->entityMappings)) { + $this->getEntityTypeMappings(); + } + + $return = array_search($token_type, $this->entityMappings); + return $return !== FALSE ? $return : ($fallback ? $token_type : FALSE); + } + + /** + * {@inheritdoc} + */ + function getTokenTypeForEntityType($entity_type, $fallback = FALSE) { + if (empty($this->entityMappings)) { + $this->getEntityTypeMappings(); + } + + return isset($this->entityMappings[$entity_type]) ? $this->entityMappings[$entity_type] : ($fallback ? $entity_type : FALSE); + } + + /** + * {@inheritdoc} + */ + public function resetInfo() { + $this->entityMappings = NULL; + } + +} diff --git a/web/modules/contrib/token/src/TokenEntityMapperInterface.php b/web/modules/contrib/token/src/TokenEntityMapperInterface.php new file mode 100644 index 000000000..5eb038ecb --- /dev/null +++ b/web/modules/contrib/token/src/TokenEntityMapperInterface.php @@ -0,0 +1,53 @@ +getDefinition('token'); + $definition->setClass('\Drupal\token\Token'); + } +} diff --git a/web/modules/contrib/token/src/TreeBuilder.php b/web/modules/contrib/token/src/TreeBuilder.php new file mode 100644 index 000000000..80ebca235 --- /dev/null +++ b/web/modules/contrib/token/src/TreeBuilder.php @@ -0,0 +1,267 @@ +tokenService = $token_service; + $this->entityMapper = $entity_mapper; + $this->cacheBackend = $cache_backend; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public function buildRenderable(array $token_types, array $options = []) { + // Set default options. + $options += [ + 'global_types' => TRUE, + 'click_insert' => TRUE, + 'show_restricted' => FALSE, + 'show_nested' => FALSE, + 'recursion_limit' => 3, + ]; + + $info = $this->tokenService->getInfo(); + if ($options['global_types']) { + $token_types = array_merge($token_types, $this->tokenService->getGlobalTokenTypes()); + } + + $element = array( + /*'#cache' => array( + 'cid' => 'tree-rendered:' . hash('sha256', serialize(array('token_types' => $token_types, 'global_types' => NULL) + $variables)), + 'tags' => array(Token::TOKEN_INFO_CACHE_TAG), + ),*/ + ); + + // @todo Find a way to use the render cache for this. + /*if ($cached_output = token_render_cache_get($element)) { + return $cached_output; + }*/ + + $tree_options = [ + 'flat' => TRUE, + 'restricted' => $options['show_restricted'], + 'nested' => $options['show_nested'], + 'depth' => $options['recursion_limit'], + ]; + + $token_tree = []; + foreach ($info['types'] as $type => $type_info) { + if (!in_array($type, $token_types)) { + continue; + } + + $token_tree[$type] = $type_info; + $token_tree[$type]['tokens'] = $this->buildTree($type, $tree_options); + } + + $element += [ + '#type' => 'token_tree_table', + '#token_tree' => $token_tree, + '#show_restricted' => $options['show_restricted'], + '#show_nested' => $options['show_nested'], + '#click_insert' => $options['click_insert'], + '#columns' => ['name', 'token', 'description'], + '#empty' => t('No tokens available'), + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function buildAllRenderable(array $options = []) { + $info = $this->tokenService->getInfo(); + $token_types = array_keys($info['types']); + + // Disable merging in global types as we will be adding in all token types + // explicitly. There is no difference in leaving this set to TRUE except for + // an additional method call which is unnecessary. + $options['global_types'] = FALSE; + return $this->buildRenderable($token_types, $options); + } + + /** + * {@inheritdoc} + */ + public function buildTree($token_type, array $options = []) { + $options += [ + 'restricted' => FALSE, + 'depth' => 4, + 'data' => [], + 'values' => FALSE, + 'flat' => FALSE, + ]; + + // Do not allow past the maximum token information depth. + $options['depth'] = min($options['depth'], static::MAX_DEPTH); + + // If $token_type is an entity, make sure we are using the actual token type. + if ($entity_token_type = $this->entityMapper->getTokenTypeForEntityType($token_type)) { + $token_type = $entity_token_type; + } + + $langcode = $this->languageManager->getCurrentLanguage()->getId(); + $tree_cid = "token_tree:{$token_type}:{$langcode}:{$options['depth']}"; + + // If we do not have this base tree in the static cache, check the cache + // otherwise generate and store it in the cache. + if (!isset($this->builtTrees[$tree_cid])) { + if ($cache = $this->cacheBackend->get($tree_cid)) { + $this->builtTrees[$tree_cid] = $cache->data; + } + else { + $options['parents'] = []; + $this->builtTrees[$tree_cid] = $this->getTokenData($token_type, $options); + $this->cacheBackend->set($tree_cid, $this->builtTrees[$tree_cid], Cache::PERMANENT, [Token::TOKEN_INFO_CACHE_TAG]); + } + } + + $tree = $this->builtTrees[$tree_cid]; + + // If the user has requested a flat tree, convert it. + if (!empty($options['flat'])) { + $tree = $this->flattenTree($tree); + } + + // Fill in token values. + if (!empty($options['values'])) { + $token_values = []; + foreach ($tree as $token => $token_info) { + if (!empty($token_info['dynamic']) || !empty($token_info['restricted'])) { + continue; + } + elseif (!isset($token_info['value'])) { + $token_values[$token_info['token']] = $token; + } + } + if (!empty($token_values)) { + $token_values = $this->tokenService->generate($token_type, $token_values, $options['data'], [], new BubbleableMetadata()); + foreach ($token_values as $token => $replacement) { + $tree[$token]['value'] = $replacement; + } + } + } + + return $tree; + } + + /** + * {@inheritdoc} + */ + public function flattenTree(array $tree) { + $result = []; + foreach ($tree as $token => $token_info) { + $result[$token] = $token_info; + if (isset($token_info['children']) && is_array($token_info['children'])) { + $result += $this->flattenTree($token_info['children']); + } + } + return $result; + } + + /** + * Generate a token tree. + * + * @param string $token_type + * The token type. + * @param array $options + * An associative array of additional options. See documentation for + * TreeBuilderInterface::buildTree() for more information. + * + * @return array + * The token data for the specified $token_type. + * + * @internal + */ + protected function getTokenData($token_type, array $options) { + $options += [ + 'parents' => [], + ]; + + $info = $this->tokenService->getInfo(); + if ($options['depth'] <= 0 || !isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) { + return []; + } + + $tree = []; + foreach ($info['tokens'][$token_type] as $token => $token_info) { + // Build the raw token string. + $token_parents = $options['parents']; + if (empty($token_parents)) { + // If the parents array is currently empty, assume the token type is its + // parent. + $token_parents[] = $token_type; + } + elseif (in_array($token, array_slice($token_parents, 1), TRUE)) { + // Prevent duplicate recursive tokens. For example, this will prevent + // the tree from generating the following tokens or deeper: + // [comment:parent:parent] + // [comment:parent:root:parent] + continue; + } + + $token_parents[] = $token; + if (!empty($token_info['dynamic'])) { + $token_parents[] = '?'; + } + $raw_token = '[' . implode(':', $token_parents) . ']'; + $tree[$raw_token] = $token_info; + $tree[$raw_token]['raw token'] = $raw_token; + + // Add the token's real name (leave out the base token type). + $tree[$raw_token]['token'] = implode(':', array_slice($token_parents, 1)); + + // Add the token's parent as its raw token value. + if (!empty($options['parents'])) { + $tree[$raw_token]['parent'] = '[' . implode(':', $options['parents']) . ']'; + } + + // Fetch the child tokens. + if (!empty($token_info['type'])) { + $child_options = $options; + $child_options['depth']--; + $child_options['parents'] = $token_parents; + $tree[$raw_token]['children'] = $this->getTokenData($token_info['type'], $child_options); + } + } + + return $tree; + } +} diff --git a/web/modules/contrib/token/src/TreeBuilderInterface.php b/web/modules/contrib/token/src/TreeBuilderInterface.php new file mode 100644 index 000000000..c4212c215 --- /dev/null +++ b/web/modules/contrib/token/src/TreeBuilderInterface.php @@ -0,0 +1,79 @@ + \Drupal::token()->replace('[current-page:title]'), + '#type' => 'markup', + ]; + return $build; + } + + /** + * Title callback for the page outputting a link. + * + * We are using a title callback instead of directly defining the title in the + * routing YML file. This is so that we could return an array instead of a + * simple string. This allows us to test if [current-page:title] works with + * render arrays and other objects as titles. + */ + public function getTitle() { + return [ + '#type' => 'markup', + '#markup' => 'Available Tokens', + ]; + } + +} diff --git a/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.info.yml b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.info.yml new file mode 100644 index 000000000..342ef6c92 --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.info.yml @@ -0,0 +1,12 @@ +type: module +name: Token Module Test +description: Testing module for token functionality. +package: Testing +# core: 8.x +hidden: TRUE + +# Information added by Drupal.org packaging script on 2017-04-29 +version: '8.x-1.0' +core: '8.x' +project: 'token' +datestamp: 1493466847 diff --git a/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.module b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.module new file mode 100644 index 000000000..c26332075 --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.module @@ -0,0 +1,32 @@ +get('token_page_tokens', array())) { + $debug += array('tokens' => array(), 'data' => array(), 'options' => array()); + foreach (array_keys($debug['tokens']) as $token) { + $debug['values'][$token] = \Drupal::token()->replace($token, $debug['data'], $debug['options']); + } + \Drupal::state()->set('token_page_tokens', $debug); + } +} + +/** + * Implements hook_ENTITY_TYPE_presave for Node entities. + */ +function token_module_test_node_presave(NodeInterface $node) { + // Transform tokens in the body. + // @see \Drupal\token\Tests\TokenMenuTest::testMenuTokens() + if ($node->hasField('body')) { + $node->body->value = \Drupal::token() + ->replace($node->body->value, ['node' => $node]); + } +} diff --git a/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.routing.yml b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.routing.yml new file mode 100644 index 000000000..fb99f1c9a --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.routing.yml @@ -0,0 +1,7 @@ +token_module_test.browse: + path: '/token_module_test/browse' + defaults: + _controller: '\Drupal\token_module_test\Controller\TokenTreeBrowseController::outputLink' + _title_callback: '\Drupal\token_module_test\Controller\TokenTreeBrowseController::getTitle' + requirements: + _access: 'TRUE' diff --git a/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.tokens.inc b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.tokens.inc new file mode 100644 index 000000000..ba8da5344 --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_module_test/token_module_test.tokens.inc @@ -0,0 +1,13 @@ + t('A test token with colons in the name'), + 'description' => NULL, + ); + + return $info; +} diff --git a/web/modules/contrib/token/tests/modules/token_user_picture/config/install/core.entity_form_display.user.user.default.yml b/web/modules/contrib/token/tests/modules/token_user_picture/config/install/core.entity_form_display.user.user.default.yml new file mode 100644 index 000000000..a30d01253 --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_user_picture/config/install/core.entity_form_display.user.user.default.yml @@ -0,0 +1,23 @@ +langcode: de +status: true +dependencies: + module: + - image + - user +id: user.user.default +targetEntityType: user +bundle: user +mode: default +content: + account: + weight: -10 + user_picture: + type: image_image + settings: + progress_indicator: throbber + preview_image_style: thumbnail + third_party_settings: { } + weight: -1 + timezone: + weight: 6 +hidden: { } diff --git a/web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.field.user.user.user_picture.yml b/web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.field.user.user.user_picture.yml new file mode 100644 index 000000000..45f6e82b7 --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.field.user.user.user_picture.yml @@ -0,0 +1,31 @@ +id: user.user.user_picture +status: true +langcode: en +entity_type: user +bundle: user +field_name: user_picture +label: Picture +description: 'Your virtual face or picture.' +required: false +default_value: { } +default_value_function: '' +settings: + file_extensions: 'png gif jpg jpeg' + file_directory: pictures + max_filesize: '30 KB' + alt_field: false + title_field: false + max_resolution: 85x85 + min_resolution: '' + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null + alt_field_required: false + title_field_required: false +field_type: image +dependencies: + config: + - field.storage.user.user_picture diff --git a/web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.storage.user.user_picture.yml b/web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.storage.user.user_picture.yml new file mode 100644 index 000000000..01d093967 --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_user_picture/config/install/field.storage.user.user_picture.yml @@ -0,0 +1,25 @@ +id: user.user_picture +status: true +langcode: en +field_name: user_picture +entity_type: user +type: image +settings: + uri_scheme: public + default_image: + uuid: null + alt: '' + title: '' + width: null + height: null +module: image +locked: false +cardinality: 1 +translatable: false +indexes: + target_id: + - target_id +dependencies: + module: + - image + - user diff --git a/web/modules/contrib/token/tests/modules/token_user_picture/token_user_picture.info.yml b/web/modules/contrib/token/tests/modules/token_user_picture/token_user_picture.info.yml new file mode 100644 index 000000000..4f8b99026 --- /dev/null +++ b/web/modules/contrib/token/tests/modules/token_user_picture/token_user_picture.info.yml @@ -0,0 +1,14 @@ +type: module +name: Token User picture +description: Testing module that provides user pictures field. +package: Testing +# core: 8.x +hidden: TRUE +dependencies: + - image + +# Information added by Drupal.org packaging script on 2017-04-29 +version: '8.x-1.0' +core: '8.x' +project: 'token' +datestamp: 1493466847 diff --git a/web/modules/contrib/token/tests/src/Kernel/ArrayTest.php b/web/modules/contrib/token/tests/src/Kernel/ArrayTest.php new file mode 100644 index 000000000..a7f83ae4d --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/ArrayTest.php @@ -0,0 +1,60 @@ + 'a', 1 => 'b', 2 => 'c', 4 => 'd'); + $tokens = array( + 'first' => 'a', + 'last' => 'd', + 'value:0' => 'a', + 'value:2' => 'c', + 'count' => 4, + 'keys' => '0, 1, 2, 4', + 'keys:value:3' => '4', + 'keys:join' => '0124', + 'reversed' => 'd, c, b, a', + 'reversed:keys' => '4, 2, 1, 0', + 'join:/' => 'a/b/c/d', + 'join' => 'abcd', + 'join:, ' => 'a, b, c, d', + 'join: ' => 'a b c d', + ); + $this->assertTokens('array', array('array' => $array), $tokens); + + // Test a mixed simple and render array. + // 2 => c, 0 => a, 4 => d, 1 => b + $array = array( + '#property' => 'value', + 0 => 'a', + 1 => array('#markup' => 'b', '#weight' => 0.01), + 2 => array('#markup' => 'c', '#weight' => -10), + 4 => array('#markup' => 'd', '#weight' => 0), + ); + $tokens = array( + 'first' => 'c', + 'last' => 'b', + 'value:0' => 'a', + 'value:2' => 'c', + 'count' => 4, + 'keys' => '2, 0, 4, 1', + 'keys:value:3' => '1', + 'keys:join' => '2041', + 'reversed' => 'b, d, a, c', + 'reversed:keys' => '1, 4, 0, 2', + 'join:/' => 'c/a/d/b', + 'join' => 'cadb', + 'join:, ' => 'c, a, d, b', + 'join: ' => 'c a d b', + ); + $this->assertTokens('array', array('array' => $array), $tokens); + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/BookTest.php b/web/modules/contrib/token/tests/src/Kernel/BookTest.php new file mode 100644 index 000000000..67a13eecd --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/BookTest.php @@ -0,0 +1,107 @@ +installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installSchema('book', array('book')); + $this->installSchema('node', array('node_access')); + $this->installConfig(array('node', 'book', 'field')); + } + + function testBookTokens() { + $book = Node::create([ + 'type' => 'book', + 'title' => 'Book Main Page', + 'book' => ['bid' => 'new'], + ]); + $book->save(); + + $page1 = Node::create([ + 'type' => 'book', + 'title' => '1st Page', + 'book' => ['bid' => $book->id(), 'pid' => $book->id()], + ]); + $page1->save(); + + $page2 = Node::create([ + 'type' => 'book', + 'title' => '2nd Page', + 'book' => ['bid' => $book->id(), 'pid' => $page1->id()], + ]); + $page2->save(); + + $book_title = $book->getTitle(); + + $tokens = [ + 'nid' => $book->id(), + 'title' => $book_title, + 'book:title' => $book_title, + 'book:root' => $book_title, + 'book:root:nid' => $book->id(), + 'book:root:title' => $book_title, + 'book:root:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(), + 'book:root:content-type' => 'Book page', + 'book:parent' => null, + 'book:parents' => null, + ]; + $this->assertTokens('node', array('node' => $book), $tokens); + + $tokens = [ + 'nid' => $page1->id(), + 'title' => $page1->getTitle(), + 'book:title' => $book_title, + 'book:root' => $book_title, + 'book:root:nid' => $book->id(), + 'book:root:title' => $book_title, + 'book:root:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(), + 'book:root:content-type' => 'Book page', + 'book:parent:nid' => $book->id(), + 'book:parent:title' => $book_title, + 'book:parent:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(), + 'book:parents:count' => 1, + 'book:parents:join:/' => $book_title, + ]; + $this->assertTokens('node', array('node' => $page1), $tokens); + + $tokens = [ + 'nid' => $page2->id(), + 'title' => $page2->getTitle(), + 'book:title' => $book_title, + 'book:root' => $book_title, + 'book:root:nid' => $book->id(), + 'book:root:title' => $book_title, + 'book:root:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(), + 'book:root:content-type' => 'Book page', + 'book:parent:nid' => $page1->id(), + 'book:parent:title' => $page1->getTitle(), + 'book:parent:url' => Url::fromRoute('entity.node.canonical', ['node' => $page1->id()], array('absolute' => TRUE))->toString(), + 'book:parents:count' => 2, + 'book:parents:join:/' => $book_title . '/' . $page1->getTitle(), + ]; + $this->assertTokens('node', array('node' => $page2), $tokens); + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/CommentTest.php b/web/modules/contrib/token/tests/src/Kernel/CommentTest.php new file mode 100644 index 000000000..499f087c3 --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/CommentTest.php @@ -0,0 +1,102 @@ +installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('comment'); + $this->installSchema('comment', ['comment_entity_statistics']); + + $node_type = NodeType::create(['type' => 'page', 'name' => t('Page')]); + $node_type->save(); + + $this->installConfig(['comment']); + + $this->addDefaultCommentField('node', 'page'); + } + + function testCommentTokens() { + $node = Node::create([ + 'type' => 'page', + 'title' => $this->randomMachineName() + ]); + $node->save(); + + $parent_comment = Comment::create([ + 'entity_id' => $node->id(), + 'entity_type' => 'node', + 'field_name' => 'comment', + 'name' => 'anonymous user', + 'mail' => 'anonymous@example.com', + 'subject' => $this->randomMachineName(), + 'body' => $this->randomMachineName(), + ]); + $parent_comment->save(); + + // Fix http://example.com/index.php/comment/1 fails 'url:path' test. + $parent_comment_path = $parent_comment->url(); + + $tokens = array( + 'url' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->setAbsolute()->toString(), + 'url:absolute' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->setAbsolute()->toString(), + 'url:relative' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->toString(), + 'url:path' => $parent_comment_path, + 'parent:url:absolute' => NULL, + ); + $this->assertTokens('comment', array('comment' => $parent_comment), $tokens); + + $comment = Comment::create([ + 'entity_id' => $node->id(), + 'pid' => $parent_comment->id(), + 'entity_type' => 'node', + 'field_name' => 'comment', + 'uid' => 1, + 'name' => 'anonymous user', + 'mail' => 'anonymous@example.com', + 'subject' => $this->randomMachineName(), + 'body' => $this->randomMachineName(), + ]); + $comment->save(); + + // Fix http://example.com/index.php/comment/1 fails 'url:path' test. + $comment_path = Url::fromRoute('entity.comment.canonical', array('comment' => $comment->id()))->toString(); + + $tokens = array( + 'url' => $comment->urlInfo('canonical', ['fragment' => "comment-{$comment->id()}"])->setAbsolute()->toString(), + 'url:absolute' => $comment->urlInfo('canonical', ['fragment' => "comment-{$comment->id()}"])->setAbsolute()->toString(), + 'url:relative' => $comment->urlInfo('canonical', ['fragment' => "comment-{$comment->id()}"])->toString(), + 'url:path' => $comment_path, + 'parent:url:absolute' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->setAbsolute()->toString(), + ); + $this->assertTokens('comment', array('comment' => $comment), $tokens); + } + +} diff --git a/web/modules/contrib/token/tests/src/Kernel/DateTest.php b/web/modules/contrib/token/tests/src/Kernel/DateTest.php new file mode 100644 index 000000000..c25bf030d --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/DateTest.php @@ -0,0 +1,35 @@ +installConfig(['system', 'token_module_test']); + } + + function testDateTokens() { + $tokens = array( + 'token_module_test' => '1984', + 'invalid_format' => NULL, + ); + + $this->assertTokens('date', array('date' => 453859200), $tokens); + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/EntityTest.php b/web/modules/contrib/token/tests/src/Kernel/EntityTest.php new file mode 100644 index 000000000..7ff471dfe --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/EntityTest.php @@ -0,0 +1,104 @@ + 'Tags', + 'vid' => 'tags', + ]); + $vocabulary->save(); + + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + + $this->vocab = $vocabulary; + } + + function testEntityMapping() { + /** @var \Drupal\token\TokenEntityMapperInterface $mapper */ + $mapper = \Drupal::service('token.entity_mapper'); + $this->assertIdentical($mapper->getEntityTypeForTokenType('node'), 'node'); + $this->assertIdentical($mapper->getEntityTypeForTokenType('term'), 'taxonomy_term'); + $this->assertIdentical($mapper->getEntityTypeForTokenType('vocabulary'), 'taxonomy_vocabulary'); + $this->assertIdentical($mapper->getEntityTypeForTokenType('invalid'), FALSE); + $this->assertIdentical($mapper->getEntityTypeForTokenType('invalid', TRUE), 'invalid'); + $this->assertIdentical($mapper->getTokenTypeForEntityType('node'), 'node'); + $this->assertIdentical($mapper->getTokenTypeForEntityType('taxonomy_term'), 'term'); + $this->assertIdentical($mapper->getTokenTypeForEntityType('taxonomy_vocabulary'), 'vocabulary'); + $this->assertIdentical($mapper->getTokenTypeForEntityType('invalid'), FALSE); + $this->assertIdentical($mapper->getTokenTypeForEntityType('invalid', TRUE), 'invalid'); + + // Test that when we send the mis-matched entity type into token_replace() + // that we still get the tokens replaced. + $vocabulary = entity_load('taxonomy_vocabulary', 'tags'); + $term = $this->addTerm($vocabulary); + $this->assertIdentical(\Drupal::token()->replace('[vocabulary:name]', array('taxonomy_vocabulary' => $vocabulary)), $vocabulary->label()); + $this->assertIdentical(\Drupal::token()->replace('[term:name][term:vocabulary:name]', array('taxonomy_term' => $term)), $term->label() . $vocabulary->label()); + } + + function addTerm(VocabularyInterface $vocabulary, array $term = array()) { + $term += array( + 'name' => Unicode::strtolower($this->randomMachineName(5)), + 'vid' => $vocabulary->id(), + ); + $term = entity_create('taxonomy_term', $term); + $term->save(); + return $term; + } + + /** + * Test the [entity:original:*] tokens. + */ + function testEntityOriginal() { + $node = Node::create(['type' => 'page', 'title' => 'Original title']); + $node->save(); + + $tokens = array( + 'nid' => $node->id(), + 'title' => 'Original title', + 'original' => NULL, + 'original:nid' => NULL, + ); + $this->assertTokens('node', array('node' => $node), $tokens); + + // Emulate the original entity property that would be available from + // node_save() and change the title for the node. + $node->original = entity_load_unchanged('node', $node->id()); + $node->title = 'New title'; + + $tokens = array( + 'nid' => $node->id(), + 'title' => 'New title', + 'original' => 'Original title', + 'original:nid' => $node->id(), + ); + $this->assertTokens('node', array('node' => $node), $tokens); + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/FieldTest.php b/web/modules/contrib/token/tests/src/Kernel/FieldTest.php new file mode 100644 index 000000000..de94699f8 --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/FieldTest.php @@ -0,0 +1,708 @@ +installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + + // Create the article content type with a text field. + $node_type = NodeType::create([ + 'type' => 'article', + ]); + $node_type->save(); + + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'node', + 'type' => 'text', + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'node', + 'bundle' => 'article', + 'label' => 'Test field', + ]); + $field->save(); + + // Create a reference field with the same name on user. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'user', + 'type' => 'entity_reference', + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'user', + 'bundle' => 'user', + 'label' => 'Test field', + ]); + $field->save(); + + $this->testFormat = FilterFormat::create([ + 'format' => 'test', + 'weight' => 1, + 'filters' => [ + 'filter_html_escape' => ['status' => TRUE], + ], + ]); + $this->testFormat->save(); + + // Create a multi-value list_string field. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'test_list', + 'entity_type' => 'node', + 'type' => 'list_string', + 'cardinality' => 2, + 'settings' => [ + 'allowed_values' => [ + 'key1' => 'value1', + 'key2' => 'value2', + ] + ], + ]); + $field_storage->save(); + + $this->field = FieldConfig::create([ + 'field_name' => 'test_list', + 'entity_type' => 'node', + 'bundle' => 'article', + ])->save(); + + // Add an untranslatable node reference field. + FieldStorageConfig::create([ + 'field_name' => 'test_reference', + 'type' => 'entity_reference', + 'entity_type' => 'node', + 'settings' => [ + 'target_type' => 'node', + ], + 'translatable' => FALSE, + ])->save(); + FieldConfig::create([ + 'field_name' => 'test_reference', + 'entity_type' => 'node', + 'bundle' => 'article', + 'label' => 'Test reference', + ])->save(); + + // Add an untranslatable taxonomy term reference field. + $this->vocabulary = $this->createVocabulary(); + + FieldStorageConfig::create([ + 'field_name' => 'test_term_reference', + 'type' => 'entity_reference', + 'entity_type' => 'node', + 'settings' => [ + 'target_type' => 'taxonomy_term', + ], + 'translatable' => FALSE, + ])->save(); + FieldConfig::create([ + 'field_name' => 'test_term_reference', + 'entity_type' => 'node', + 'bundle' => 'article', + 'label' => 'Test term reference', + 'settings' => [ + 'handler' => 'default:taxonomy_term', + 'handler_settings' => [ + 'target_bundles' => [ + $this->vocabulary->id() => $this->vocabulary->id(), + ], + ], + ], + ])->save(); + + // Add a field to terms of the created vocabulary. + $storage = FieldStorageConfig::create([ + 'field_name' => 'term_field', + 'entity_type' => 'taxonomy_term', + 'type' => 'text', + ]); + $storage->save(); + $field = FieldConfig::create([ + 'field_name' => 'term_field', + 'entity_type' => 'taxonomy_term', + 'bundle' => $this->vocabulary->id(), + ]); + $field->save(); + + // Add a second language. + $language = ConfigurableLanguage::create([ + 'id' => 'de', + 'label' => 'German', + ]); + $language->save(); + + // Add a datetime field. + $field_datetime_storage = FieldStorageConfig::create(array( + 'field_name' => 'field_datetime', + 'type' => 'datetime', + 'entity_type' => 'node', + 'settings' => array('datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME), + )); + $field_datetime_storage->save(); + $field_datetime = FieldConfig::create([ + 'field_storage' => $field_datetime_storage, + 'bundle' => 'article', + ]); + $field_datetime->save(); + + // Add a daterange field. + $field_daterange_storage = FieldStorageConfig::create(array( + 'field_name' => 'field_daterange', + 'type' => 'daterange', + 'entity_type' => 'node', + 'settings' => array('datetime_type' => DateRangeItem::DATETIME_TYPE_DATETIME), + )); + $field_daterange_storage->save(); + $field_daterange = FieldConfig::create([ + 'field_storage' => $field_daterange_storage, + 'bundle' => 'article', + ]); + $field_daterange->save(); + } + + /** + * Tests [entity:field_name] tokens. + */ + public function testEntityFieldTokens() { + // Create a node with a value in its fields and test its tokens. + $entity = Node::create([ + 'title' => 'Test node title', + 'type' => 'article', + 'test_field' => [ + 'value' => 'foo', + 'format' => $this->testFormat->id(), + ], + 'test_list' => [ + 'value1', + 'value2', + ], + ]); + $entity->save(); + $this->assertTokens('node', ['node' => $entity], [ + 'test_field' => Markup::create('foo'), + 'test_field:0' => Markup::create('foo'), + 'test_field:0:value' => 'foo', + 'test_field:value' => 'foo', + 'test_field:0:format' => $this->testFormat->id(), + 'test_field:format' => $this->testFormat->id(), + 'test_list:0' => Markup::create('value1'), + 'test_list:1' => Markup::create('value2'), + 'test_list:0:value' => Markup::create('value1'), + 'test_list:value' => Markup::create('value1'), + 'test_list:1:value' => Markup::create('value2'), + ]); + + // Verify that no third token was generated for the list_string field. + $this->assertNoTokens('node', ['node' => $entity], [ + 'test_list:2', + 'test_list:2:value', + ]); + + // Test the test_list token metadata. + $tokenService = \Drupal::service('token'); + $token_info = $tokenService->getTokenInfo('node', 'test_list'); + $this->assertEqual($token_info['name'], 'test_list'); + $this->assertEqual($token_info['module'], 'token'); + $this->assertEqual($token_info['type'], 'list'); + $typeInfo = $tokenService->getTypeInfo('list'); + $this->assertEqual($typeInfo['name'], 'List of test_list values'); + $this->assertEqual($typeInfo['type'], 'list'); + + // Create a node type that does not have test_field field. + $node_type = NodeType::create([ + 'type' => 'page', + ]); + $node_type->save(); + + $node_without_test_field = Node::create([ + 'title' => 'Node without test_field', + 'type' => 'page', + ]); + $node_without_test_field->save(); + + // Ensure that trying to generate tokens for a non-existing field does not + // throw an exception. + $this->assertNoTokens('node', ['node' => $node_without_test_field], ['test_field']); + + // Create a node without a value in the text field and test its token. + $entity = Node::create([ + 'title' => 'Test node title', + 'type' => 'article', + ]); + $entity->save(); + + $this->assertNoTokens('node', ['node' => $entity], [ + 'test_field', + ]); + } + + /** + * Tests the token metadata for a field token. + */ + public function testFieldTokenInfo() { + /** @var \Drupal\token\Token $tokenService */ + $tokenService = \Drupal::service('token'); + + // Test the token info of the text field of the artcle content type. + $token_info = $tokenService->getTokenInfo('node', 'test_field'); + $this->assertEqual($token_info['name'], 'Test field', 'The token info name is correct.'); + $this->assertEqual($token_info['description'], 'Text (formatted) field.', 'The token info description is correct.'); + $this->assertEqual($token_info['module'], 'token', 'The token info module is correct.'); + + // Now create two more content types that share the field but the last + // of them sets a different label. This should show an alternative label + // at the token info. + $node_type = NodeType::create([ + 'type' => 'article2', + ]); + $node_type->save(); + $field = FieldConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'node', + 'bundle' => 'article2', + 'label' => 'Test field', + ]); + $field->save(); + + $node_type = NodeType::create([ + 'type' => 'article3', + ]); + $node_type->save(); + $field = FieldConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'node', + 'bundle' => 'article3', + 'label' => 'Different test field', + ]); + $field->save(); + + $token_info = $tokenService->getTokenInfo('node', 'test_field'); + $this->assertEqual($token_info['name'], 'Test field', 'The token info name is correct.'); + $this->assertEqual((string) $token_info['description'], 'Text (formatted) field. Also known as Different test field.', 'When a field is used in several bundles with different labels, this is noted at the token info description.'); + $this->assertEqual($token_info['module'], 'token', 'The token info module is correct.'); + $this->assertEqual($token_info['type'], 'node-test_field', 'The field property token info type is correct.'); + + // Test field property token info. + $token_info = $tokenService->getTokenInfo('node-test_field', 'value'); + $this->assertEqual($token_info['name'], 'Text', 'The field property token info name is correct.'); + // This particular field property description happens to be empty. + $this->assertEqual((string) $token_info['description'], '', 'The field property token info description is correct.'); + $this->assertEqual($token_info['module'], 'token', 'The field property token info module is correct.'); + } + + /** + * Test tokens on node with the token view mode overriding default formatters. + */ + public function testTokenViewMode() { + $value = 'A really long string that should be trimmed by the special formatter on token view we are going to have.'; + + // The formatter we are going to use will eventually call Unicode::strlen. + // This expects that the Unicode has already been explicitly checked, which + // happens in DrupalKernel. But since that doesn't run in kernel tests, we + // explicitly call this here. + Unicode::check(); + + // Create a node with a value in the text field and test its token. + $entity = Node::create([ + 'title' => 'Test node title', + 'type' => 'article', + 'test_field' => [ + 'value' => $value, + 'format' => $this->testFormat->id(), + ], + ]); + $entity->save(); + + $this->assertTokens('node', ['node' => $entity], [ + 'test_field' => Markup::create($value), + ]); + + // Now, create a token view mode which sets a different format for + // test_field. When replacing tokens, this formatter should be picked over + // the default formatter for the field type. + // @see field_tokens(). + $view_mode = EntityViewMode::create([ + 'id' => 'node.token', + 'targetEntityType' => 'node', + ]); + $view_mode->save(); + $entity_display = entity_get_display('node', 'article', 'token'); + $entity_display->setComponent('test_field', [ + 'type' => 'text_trimmed', + 'settings' => [ + 'trim_length' => 50, + ] + ]); + $entity_display->save(); + + $this->assertTokens('node', ['node' => $entity], [ + 'test_field' => Markup::create(substr($value, 0, 50)), + ]); + } + + /** + * Test that tokens are properly created for an entity's base fields. + */ + public function testBaseFieldTokens() { + // Create a new contact_message entity and verify that tokens are generated + // for its base fields. The contact_message entity type is used because it + // provides no tokens by default. + $contact_form = ContactForm::create([ + 'id' => 'form_id', + ]); + $contact_form->save(); + + $entity = Message::create([ + 'contact_form' => 'form_id', + 'uuid' => '123', + 'langcode' => 'en', + 'name' => 'Test name', + 'mail' => 'Test mail', + 'subject' => 'Test subject', + 'message' => 'Test message', + 'copy' => FALSE, + ]); + $entity->save(); + $this->assertTokens('contact_message', ['contact_message' => $entity], [ + 'uuid' => Markup::create('123'), + 'langcode' => Markup::create('English'), + 'name' => Markup::create('Test name'), + 'mail' => Markup::create('Test mail'), + 'subject' => Markup::create('Test subject'), + 'message' => Markup::create('Test message'), + 'copy' => 'Off', + ]); + + // Test the metadata of one of the tokens. + $tokenService = \Drupal::service('token'); + $token_info = $tokenService->getTokenInfo('contact_message', 'subject'); + $this->assertEquals($token_info['name'], 'Subject'); + $this->assertEquals($token_info['description'], 'Text (plain) field.'); + $this->assertEquals($token_info['module'], 'token'); + + // Verify that node entity type doesn't have a uid token. + $this->assertNull($tokenService->getTokenInfo('node', 'uid')); + } + + /* + * Tests chaining entity reference tokens. + */ + public function testEntityReferenceTokens() { + $reference = Node::create([ + 'title' => 'Test node to reference', + 'type' => 'article', + 'test_field' => [ + 'value' => 'foo', + 'format' => $this->testFormat->id(), + ] + ]); + $reference->save(); + $term_reference_field_value = $this->randomString(); + $term_reference = $this->createTerm($this->vocabulary, [ + 'name' => 'Term to reference', + 'term_field' => [ + 'value' => $term_reference_field_value, + 'format' => $this->testFormat->id(), + ], + ]); + $entity = Node::create([ + 'title' => 'Test entity reference', + 'type' => 'article', + 'test_reference' => ['target_id' => $reference->id()], + 'test_term_reference' => ['target_id' => $term_reference->id()], + ]); + $entity->save(); + + $this->assertTokens('node', ['node' => $entity], [ + 'test_reference:entity:title' => Markup::create('Test node to reference'), + 'test_reference:entity:test_field' => Markup::create('foo'), + 'test_term_reference:entity:term_field' => Html::escape($term_reference_field_value), + 'test_reference:target_id' => $reference->id(), + 'test_term_reference:target_id' => $term_reference->id(), + 'test_term_reference:entity:url:path' => '/' . $term_reference->toUrl('canonical')->getInternalPath(), + // Expects the entity's label to be returned for :entity tokens. + 'test_reference:entity' => $reference->label(), + 'test_term_reference:entity' => $term_reference->label(), + ]); + + // Test some non existent tokens. + $this->assertNoTokens('node', ['node' => $entity], [ + 'test_reference:1:title', + 'test_reference:entity:does_not_exist', + 'test_reference:does_not:exist', + 'test_term_reference:does_not_exist', + 'test_term_reference:does:not:exist', + 'test_term_reference:does_not_exist:0', + 'non_existing_field:entity:title', + ]); + + /** @var \Drupal\token\Token $token_service */ + $token_service = \Drupal::service('token'); + + $token_info = $token_service->getTokenInfo('node', 'test_reference'); + $this->assertEquals('Test reference', $token_info['name']); + $this->assertEquals('Entity reference field.', (string) $token_info['description']); + $this->assertEquals('token', $token_info['module']); + $this->assertEquals('node-test_reference', $token_info['type']); + + // Test target_id field property token info. + $token_info = $token_service->getTokenInfo('node-test_reference', 'target_id'); + $this->assertEquals('Content ID', $token_info['name']); + $this->assertEquals('token', $token_info['module']); + $this->assertEquals('token', $token_info['module']); + + // Test entity field property token info. + $token_info = $token_service->getTokenInfo('node-test_reference', 'entity'); + $this->assertEquals('Content', $token_info['name']); + $this->assertEquals('The referenced entity', $token_info['description']); + $this->assertEquals('token', $token_info['module']); + $this->assertEquals('node', $token_info['type']); + + // Test entity field property token info of the term reference. + $token_info = $token_service->getTokenInfo('node-test_term_reference', 'entity'); + $this->assertEquals('Taxonomy term', $token_info['name']); + $this->assertEquals('The referenced entity', $token_info['description']); + $this->assertEquals('token', $token_info['module']); + $this->assertEquals('term', $token_info['type']); + + } + + /** + * Tests support for cardinality > 1 for entity reference tokens. + */ + public function testEntityReferenceTokensCardinality() { + /** @var \Drupal\field\FieldStorageConfigInterface $storage */ + $storage = FieldStorageConfig::load('node.test_term_reference'); + $storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $storage->save(); + + // Add a few terms. + $terms = []; + $terms_value = []; + foreach (range(1, 3) as $i) { + $terms_value[$i] = $this->randomString(); + $terms[$i] = $this->createTerm($this->vocabulary, [ + 'name' => $this->randomString(), + 'term_field' => [ + 'value' => $terms_value[$i], + 'format' => $this->testFormat->id(), + ], + ]); + } + + $entity = Node::create([ + 'title' => 'Test multivalue chained tokens', + 'type' => 'article', + 'test_term_reference' => [ + ['target_id' => $terms[1]->id()], + ['target_id' => $terms[2]->id()], + ['target_id' => $terms[3]->id()], + ], + ]); + $entity->save(); + + $this->assertTokens('node', ['node' => $entity], [ + 'test_term_reference:0:entity:term_field' => Html::escape($terms[1]->term_field->value), + 'test_term_reference:1:entity:term_field' => Html::escape($terms[2]->term_field->value), + 'test_term_reference:2:entity:term_field' => Html::escape($terms[3]->term_field->value), + 'test_term_reference:0:target_id' => $terms[1]->id(), + 'test_term_reference:1:target_id' => $terms[2]->id(), + 'test_term_reference:2:target_id' => $terms[3]->id(), + // Expects the entity's label to be returned for :entity tokens. + 'test_term_reference:0:entity' => $terms[1]->label(), + 'test_term_reference:1:entity' => $terms[2]->label(), + 'test_term_reference:2:entity' => $terms[3]->label(), + // To make sure tokens without an explicit delta can also be replaced in + // the same token replacement call. + 'test_term_reference:entity:term_field' => Html::escape($terms[1]->term_field->value), + 'test_term_reference:target_id' => $terms[1]->id(), + ]); + + // Test some non existent tokens. + $this->assertNoTokens('node', ['node' => $entity], [ + 'test_term_reference:3:term_field', + 'test_term_reference:0:does_not_exist', + 'test_term_reference:1:does:not:exist', + 'test_term_reference:1:2:does_not_exist', + ]); + } + + /** + * Test tokens for multilingual fields and entities. + */ + public function testMultilingualFields() { + // Create an english term and add a german translation for it. + $term = $this->createTerm($this->vocabulary, [ + 'name' => 'english-test-term', + 'langcode' => 'en', + 'term_field' => [ + 'value' => 'english-term-field-value', + 'format' => $this->testFormat->id(), + ], + ]); + $term->addTranslation('de', [ + 'name' => 'german-test-term', + 'term_field' => [ + 'value' => 'german-term-field-value', + 'format' => $this->testFormat->id(), + ], + ])->save(); + $german_term = $term->getTranslation('de'); + + // Create an english node, add a german translation for it and add the + // english term to the english node's entity reference field and the + // german term to the german's entity reference field. + $node = Node::create([ + 'title' => 'english-node-title', + 'type' => 'article', + 'test_term_reference' => [ + 'target_id' => $term->id(), + ], + 'test_field' => [ + 'value' => 'test-english-field', + 'format' => $this->testFormat->id(), + ], + ]); + $node->addTranslation('de', [ + 'title' => 'german-node-title', + 'test_term_reference' => [ + 'target_id' => $german_term->id(), + ], + 'test_field' => [ + 'value' => 'test-german-field', + 'format' => $this->testFormat->id(), + ], + ])->save(); + + // Verify the :title token of the english node and the :name token of the + // english term it refers to. Also verify the value of the term's field. + $this->assertTokens('node', ['node' => $node], [ + 'title' => 'english-node-title', + 'test_term_reference:entity:name' => 'english-test-term', + 'test_term_reference:entity:term_field:value' => 'english-term-field-value', + 'test_term_reference:entity:term_field' => 'english-term-field-value', + 'test_field' => 'test-english-field', + 'test_field:value' => 'test-english-field', + ]); + + // Same test for the german node and its german term. + $german_node = $node->getTranslation('de'); + $this->assertTokens('node', ['node' => $german_node], [ + 'title' => 'german-node-title', + 'test_term_reference:entity:name' => 'german-test-term', + 'test_term_reference:entity:term_field:value' => 'german-term-field-value', + 'test_term_reference:entity:term_field' => 'german-term-field-value', + 'test_field' => 'test-german-field', + 'test_field:value' => 'test-german-field', + ]); + + // If the langcode is specified, it should have priority over the node's + // active language. + $tokens = [ + 'test_field' => 'test-german-field', + 'test_field:value' => 'test-german-field', + 'test_term_reference:entity:term_field' => 'german-term-field-value', + 'test_term_reference:entity:term_field:value' => 'german-term-field-value', + ]; + $this->assertTokens('node', ['node' => $node], $tokens, ['langcode' => 'de']); + } + + /** + * Tests support for a datetime fields. + */ + public function testDatetimeFieldTokens() { + + $node = Node::create([ + 'title' => 'Node for datetime field', + 'type' => 'article', + ]); + + $node->set('field_datetime', '1925-09-28T00:00:00')->save(); + $this->assertTokens('node', ['node' => $node], [ + 'field_datetime:date:custom:Y' => '1925', + 'field_datetime:date:html_month' => '1925-09', + 'field_datetime:date' => $node->field_datetime->date->getTimestamp(), + ]); + } + + /** + * Tests support for a daterange fields. + */ + public function testDatetimeRangeFieldTokens() { + + $node = Node::create([ + 'title' => 'Node for daterange field', + 'type' => 'article', + ]); + + $node->field_daterange->value = '2013-12-22T00:00:00'; + $node->field_daterange->end_value = '2016-08-26T00:00:00'; + $node->save(); + $this->assertTokens('node', ['node' => $node], [ + 'field_daterange:start_date:html_month' => '2013-12', + 'field_daterange:start_date:custom:Y' => '2013', + 'field_daterange:end_date:custom:Y' => '2016', + 'field_daterange:start_date' => $node->field_daterange->start_date->getTimestamp(), + ]); + } + +} diff --git a/web/modules/contrib/token/tests/src/Kernel/FileTest.php b/web/modules/contrib/token/tests/src/Kernel/FileTest.php new file mode 100644 index 000000000..3a6a7fd64 --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/FileTest.php @@ -0,0 +1,55 @@ +installEntitySchema('file'); + } + + function testFileTokens() { + // Create a test file object. + $file = entity_create('file', array( + 'fid' => 1, + 'filename' => 'test.png', + 'filesize' => 100, + 'uri' => 'public://images/test.png', + 'filemime' => 'image/png', + )); + + $tokens = array( + 'basename' => 'test.png', + 'extension' => 'png', + 'size-raw' => 100, + ); + $this->assertTokens('file', array('file' => $file), $tokens); + + // Test a file with no extension and a fake name. + $file->filename = 'Test PNG image'; + $file->uri = 'public://images/test'; + + $tokens = array( + 'basename' => 'test', + 'extension' => '', + 'size-raw' => 100, + ); + $this->assertTokens('file', array('file' => $file), $tokens); + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/KernelTestBase.php b/web/modules/contrib/token/tests/src/Kernel/KernelTestBase.php new file mode 100644 index 000000000..a9657f638 --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/KernelTestBase.php @@ -0,0 +1,33 @@ +installSchema('system', ['router', 'url_alias']); + \Drupal::service('router.builder')->rebuild(); + $this->installConfig(['system']); + } + +} diff --git a/web/modules/contrib/token/tests/src/Kernel/NodeTest.php b/web/modules/contrib/token/tests/src/Kernel/NodeTest.php new file mode 100644 index 000000000..e44b9abd0 --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/NodeTest.php @@ -0,0 +1,99 @@ +installEntitySchema('user'); + $this->installEntitySchema('node'); + + $node_type = NodeType::create([ + 'type' => 'page', + 'name' => 'Basic page', + 'description' => "Use basic pages for your static content, such as an 'About us' page.", + ]); + $node_type->save(); + $node_type = NodeType::create([ + 'type' => 'article', + 'name' => 'Article', + 'description' => "Use articles for time-sensitive content like news, press releases or blog posts.", + ]); + $node_type->save(); + } + + function testNodeTokens() { + $page = Node::create([ + 'type' => 'page', + 'title' => 'Source Title', + 'revision_log' => $this->randomMachineName(), + 'path' => array('alias' => '/content/source-node') + ]); + $page->save(); + $tokens = array( + 'log' => $page->revision_log->value, + 'url:path' => '/content/source-node', + 'url:absolute' => Url::fromRoute('entity.node.canonical', ['node' => $page->id()], array('absolute' => TRUE))->toString(), + 'url:relative' => Url::fromRoute('entity.node.canonical', ['node' => $page->id()], array('absolute' => FALSE))->toString(), + 'url:unaliased:path' => "/node/{$page->id()}", + 'content-type' => 'Basic page', + 'content-type:name' => 'Basic page', + 'content-type:machine-name' => 'page', + 'content-type:description' => "Use basic pages for your static content, such as an 'About us' page.", + 'content-type:node-count' => 1, + 'content-type:edit-url' => Url::fromRoute('entity.node_type.edit_form', ['node_type' => 'page'], array('absolute' => TRUE))->toString(), + 'source:title' => 'Source Title', + // Deprecated tokens. + 'type' => 'page', + 'type-name' => 'Basic page', + 'url:alias' => '/content/source-node', + ); + $this->assertTokens('node', array('node' => $page), $tokens); + + $article = Node::create([ + 'type' => 'article', + 'title' => 'Source Title', + ]); + $article->save(); + $tokens = array( + 'log' => '', + 'url:path' => "/node/{$article->id()}", + 'url:absolute' => Url::fromRoute('entity.node.canonical', ['node' => $article->id()], array('absolute' => TRUE))->toString(), + 'url:relative' => Url::fromRoute('entity.node.canonical', ['node' => $article->id()], array('absolute' => FALSE))->toString(), + 'url:unaliased:path' => "/node/{$article->id()}", + 'content-type' => 'Article', + 'content-type:name' => 'Article', + 'content-type:machine-name' => 'article', + 'content-type:description' => "Use articles for time-sensitive content like news, press releases or blog posts.", + 'content-type:node-count' => 1, + 'content-type:edit-url' => Url::fromRoute('entity.node_type.edit_form', ['node_type' => 'article'], array('absolute' => TRUE))->toString(), + 'source:title' => 'Source Title', + // Deprecated tokens. + 'type' => 'article', + 'type-name' => 'Article', + 'url:alias' => "/node/{$article->id()}", + ); + $this->assertTokens('node', array('node' => $article), $tokens); + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/RandomTest.php b/web/modules/contrib/token/tests/src/Kernel/RandomTest.php new file mode 100644 index 000000000..a4b09303d --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/RandomTest.php @@ -0,0 +1,27 @@ + '[0-9]{1,}', + 'hash:md5' => '[0-9a-f]{32}', + 'hash:sha1' => '[0-9a-f]{40}', + 'hash:sha256' => '[0-9a-f]{64}', + 'hash:invalid-algo' => NULL, + ); + + $first_set = $this->assertTokens('random', array(), $tokens, array('regex' => TRUE)); + $second_set = $this->assertTokens('random', array(), $tokens, array('regex' => TRUE)); + foreach ($first_set as $token => $value) { + $this->assertNotIdentical($first_set[$token], $second_set[$token]); + } + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/TaxonomyTest.php b/web/modules/contrib/token/tests/src/Kernel/TaxonomyTest.php new file mode 100644 index 000000000..73274df08 --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/TaxonomyTest.php @@ -0,0 +1,151 @@ +installEntitySchema('taxonomy_term'); + + // Create the default tags vocabulary. + $vocabulary = Vocabulary::create([ + 'name' => 'Tags', + 'vid' => 'tags', + ]); + $vocabulary->save(); + $this->vocab = $vocabulary; + } + + /** + * Test the additional taxonomy term tokens. + */ + function testTaxonomyTokens() { + $root_term = $this->addTerm($this->vocab, array('name' => 'Root term', 'path' => array('alias' => '/root-term'))); + $tokens = array( + 'url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $root_term->id()], array('absolute' => TRUE))->toString(), + 'url:absolute' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $root_term->id()], array('absolute' => TRUE))->toString(), + 'url:relative' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $root_term->id()], array('absolute' => FALSE))->toString(), + 'url:path' => '/root-term', + 'url:unaliased:path' => "/taxonomy/term/{$root_term->id()}", + 'edit-url' => Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $root_term->id()], array('absolute' => TRUE))->toString(), + 'parents' => NULL, + 'parents:count' => NULL, + 'parents:keys' => NULL, + 'root' => NULL, + // Deprecated tokens + 'url:alias' => '/root-term', + ); + $this->assertTokens('term', array('term' => $root_term), $tokens); + + $parent_term = $this->addTerm($this->vocab, array('name' => 'Parent term', 'parent' => $root_term->id())); + $tokens = array( + 'url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $parent_term->id()], array('absolute' => TRUE))->toString(), + 'url:absolute' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $parent_term->id()], array('absolute' => TRUE))->toString(), + 'url:relative' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $parent_term->id()], array('absolute' => FALSE))->toString(), + 'url:path' => "/taxonomy/term/{$parent_term->id()}", + 'url:unaliased:path' => "/taxonomy/term/{$parent_term->id()}", + 'edit-url' => Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $parent_term->id()], array('absolute' => TRUE))->toString(), + 'parents' => 'Root term', + 'parents:count' => 1, + 'parents:keys' => $root_term->id(), + 'root' => $root_term->label(), + 'root:tid' => $root_term->id(), + // Deprecated tokens + 'url:alias' => "/taxonomy/term/{$parent_term->id()}", + ); + $this->assertTokens('term', array('term' => $parent_term), $tokens); + + $term = $this->addTerm($this->vocab, array('name' => 'Test term', 'parent' => $parent_term->id())); + $tokens = array( + 'parents' => 'Root term, Parent term', + 'parents:count' => 2, + 'parents:keys' => implode(', ', array($root_term->id(), $parent_term->id())), + ); + $this->assertTokens('term', array('term' => $term), $tokens); + } + + /** + * Test the additional vocabulary tokens. + */ + function testVocabularyTokens() { + $vocabulary = $this->vocab; + $tokens = array( + 'machine-name' => 'tags', + 'edit-url' => Url::fromRoute('entity.taxonomy_vocabulary.edit_form', ['taxonomy_vocabulary' => $vocabulary->id()], array('absolute' => TRUE))->toString(), + ); + $this->assertTokens('vocabulary', array('vocabulary' => $vocabulary), $tokens); + } + + function addVocabulary(array $vocabulary = array()) { + $vocabulary += array( + 'name' => Unicode::strtolower($this->randomMachineName(5)), + 'nodes' => array('article' => 'article'), + ); + $vocabulary = entity_create('taxonomy_vocabulary', $vocabulary)->save(); + return $vocabulary; + } + + function addTerm($vocabulary, array $term = array()) { + $term += array( + 'name' => Unicode::strtolower($this->randomMachineName(5)), + 'vid' => $vocabulary->id(), + ); + $term = entity_create('taxonomy_term', $term); + $term->save(); + return $term; + } + + /** + * Test the multilingual terms. + */ + function testMultilingualTerms() { + // Add a second language. + $language = ConfigurableLanguage::createFromLangcode('de'); + $language->save(); + + // Create an english parent term and add a german translation for it. + $parent_term = $this->addTerm($this->vocab, [ + 'name' => 'english-parent-term', + 'langcode' => 'en', + ]); + $parent_term->addTranslation('de', [ + 'name' => 'german-parent-term', + ])->save(); + + // Create a term related to the parent term. + $child_term = $this->addTerm($this->vocab, [ + 'name' => 'english-child-term', + 'langcode' => 'en', + 'parent' => $parent_term->id(), + ]); + $child_term->addTranslation('de', [ + 'name' => 'german-child-term', + ])->save(); + + // Expect the parent term to be in the specified language. + $this->assertTokens('term', array('term' => $child_term), ['parents' => 'german-parent-term'], ['langcode' => 'de']); + } +} diff --git a/web/modules/contrib/token/tests/src/Kernel/UnitTest.php b/web/modules/contrib/token/tests/src/Kernel/UnitTest.php new file mode 100644 index 000000000..c91f19433 --- /dev/null +++ b/web/modules/contrib/token/tests/src/Kernel/UnitTest.php @@ -0,0 +1,119 @@ +tokenService = \Drupal::token(); + } + + /** + * Test invalid tokens. + */ + public function testGetInvalidTokens() { + $tests = array(); + $tests[] = array( + 'valid tokens' => array( + '[node:title]', + '[node:created:short]', + '[node:created:custom:invalid]', + '[node:created:custom:mm-YYYY]', + '[node:colons:in:name]', + '[site:name]', + '[site:slogan]', + '[current-date:short]', + '[current-user:uid]', + '[current-user:ip-address]', + ), + 'invalid tokens' => array( + '[node:title:invalid]', + '[node:created:invalid]', + '[node:created:short:invalid]', + '[node:colons:in:name:invalid]', + '[invalid:title]', + '[site:invalid]', + '[user:ip-address]', + '[user:uid]', + '[comment:cid]', + // Deprecated tokens + '[node:tnid]', + '[node:type]', + '[node:type-name]', + '[date:short]', + ), + 'types' => array('node'), + ); + $tests[] = array( + 'valid tokens' => array( + '[node:title]', + '[node:created:short]', + '[node:created:custom:invalid]', + '[node:created:custom:mm-YYYY]', + '[node:colons:in:name]', + '[site:name]', + '[site:slogan]', + '[user:uid]', + '[current-date:short]', + '[current-user:uid]', + ), + 'invalid tokens' => array( + '[node:title:invalid]', + '[node:created:invalid]', + '[node:created:short:invalid]', + '[node:colons:in:name:invalid]', + '[invalid:title]', + '[site:invalid]', + '[user:ip-address]', + '[comment:cid]', + // Deprecated tokens + '[node:tnid]', + '[node:type]', + '[node:type-name]', + ), + 'types' => array('all'), + ); + + foreach ($tests as $test) { + $tokens = array_merge($test['valid tokens'], $test['invalid tokens']); + shuffle($tokens); + + $invalid_tokens = $this->tokenService->getInvalidTokensByContext(implode(' ', $tokens), $test['types']); + + sort($invalid_tokens); + sort($test['invalid tokens']); + $this->assertEqual($invalid_tokens, $test['invalid tokens'], 'Invalid tokens detected properly: ' . implode(', ', $invalid_tokens)); + } + } + + /** + * Test that tokens are generated only for content entities. + */ + public function testContentEntityOnlyTokens() { + // Verify that type and token info for a config entity is not generated. + $this->assertNull($this->tokenService->getTokenInfo('user_role', 'original')); + $this->assertNull($this->tokenService->getTokenInfo('user_role', 'url')); + $this->assertNull($this->tokenService->getTypeInfo('user_role')); + } +} diff --git a/web/modules/contrib/token/token.drush.inc b/web/modules/contrib/token/token.drush.inc new file mode 100644 index 000000000..b31b8d764 --- /dev/null +++ b/web/modules/contrib/token/token.drush.inc @@ -0,0 +1,20 @@ + $problem) { + if (!empty($problem['problems'])) { + $problems = array_unique($problem['problems']); + + $build = [ + '#theme' => 'item_list', + '#items' => $problems, + ]; + + $requirements['token-' . $problem_key] = array( + 'title' => $problem['label'], + 'value' => \Drupal::service('renderer')->renderPlain($build), + 'severity' => $problem['severity'], + ); + } + } + } + + return $requirements; +} + +/** + * Implements hook_install(). + */ +function token_install() { + // Create a token view mode for each entity type. + $info = \Drupal::entityTypeManager()->getDefinitions(); + foreach ($info as $entity_type => $entity_type_info) { + // We're only interested in entity types with a view builder. + if (!$entity_type_info->getViewBuilderClass()) { + continue; + } + // Try to find a token view mode for that entity type. + $storage = \Drupal::entityTypeManager()->getStorage('entity_view_mode'); + // Add a token view mode if it does not already exist. + if (!$storage->load("$entity_type.token")) { + $storage->create(array( + 'targetEntityType' => $entity_type, + 'id' => "$entity_type.token", + 'status' => TRUE, + 'label' => t('Token'), + ))->save(); + } + } +} + +/** + * Build a list of Drupal 6 tokens and their Drupal 7 token names. + */ +function _token_upgrade_token_list() { + $tokens = array( + // Global tokens + 'user-name' => 'current-user:name', + 'user-id' => 'current-user:id', + 'user-mail' => 'current-user:mail', + 'site-url' => 'site:url', + 'site-name' => 'site:name', + 'site-slogan' => 'site:slogan', + 'site-mission' => 'site:mission', + 'site-mail' => 'site:mail', + 'site-date' => 'date:short', + //'site-date-' => '', // Date tokens expanded below + 'current-page-path' => 'current-page:path', + 'current-page-url' => 'current-page:url', + 'page-number' => 'current-page:page-number', + + // Comment tokens + 'comment-cid' => 'comment:cid', + 'comment-nid' => 'comment:node:nid', + 'comment-title' => 'comment:title', + 'comment-body' => 'comment:body', + 'comment-author-name' => 'comment:author:name', + 'comment-author-mail' => 'comment:author:mail', + //'comment-body-format' => '', + //'comment-' => '', // Date tokens expanded below + 'comment-node-title' => 'comment:node', + + // Node tokens + 'nid' => 'node:nid', + 'type' => 'node:type', + 'type-name' => 'node:type-name', + 'language' => 'node:language', + 'title' => 'node:title', + 'author-uid' => 'node:author:uid', + 'author-name' => 'node:author:name', + 'author-mail' => 'node:author:mail', + 'node_comment_count' => 'node:comment-count', + 'unread_comment_count' => 'node:comment-count-new', + 'log' => 'node:log', + //'' => '', // Date tokens expanded below + //'mod-' => '', // Date tokens expanded below + 'menupath' => 'node:menu-link:parent:path][node:menu-link', + 'menu' => 'node:menu-link:menu-name', + 'menu-link-title' => 'node:menu-link', + 'menu-link-mlid' => 'node:menu-link:mlid', + 'menu-link-plid' => 'node:menu-link:parent:mlid', + //'term' => 'node:term', + //'term-id' => 'node:term:tid', + //'vocab' => 'node:term:vocabulary', + //'vocab-id' => 'node:term:vocabulary:vid', + + // Book tokens + //'book' => 'node:book', + //'book_id' => 'node:book:bid', + //'bookpath' => 'node:book:path', + + // Taxonomy tokens + 'tid' => 'term:tid', + 'cat' => 'term:name', + 'cat-description' => 'term:description', + 'vid' => 'term:vocabulary:vid', + 'vocab' => 'term:vocabulary', + 'vocab-description' => 'term:vocabulary:description', + + // User tokens + 'user' => 'user:name', + 'uid' => 'user:uid', + 'mail' => 'user:mail', + 'reg-date' => 'user:created', + 'reg-since' => 'user:created:since', + //'user-created' => '', // Date tokens expanded below + 'log-date' => 'user:last-login', + 'log-since' => 'user:last-login:since', + //'user-last-login' => '', // Date tokens expanded below + //'date-in-tz' => '', + 'account-url' => 'user:url', + 'account-edit' => 'user:edit-url', + ); + + // Account for date tokens which need to be expanded. + $tokens += _token_upgrade_token_date_list('site-', 'site:date'); + $tokens += _token_upgrade_token_date_list('', 'node:created'); + $tokens += _token_upgrade_token_date_list('mod-', 'node:changed'); + //$tokens += _token_upgrade_token_date_list('node-revision-', 'node:changed'); + $tokens += _token_upgrade_token_date_list('comment-', 'comment:created'); + $tokens += _token_upgrade_token_date_list('user-register-', 'user:created'); + $tokens += _token_upgrade_token_date_list('user-last-login-', 'user:last-login'); + + return $tokens; +} + +/** + * Build a list of Drupal 6 date tokens and their Drupal 7 token names. + */ +function _token_upgrade_token_date_list($old_token, $new_token) { + $tokens = array(); + $formats = array( + 'yyyy' => 'Y', + 'yy' => 'y', + 'month' => 'F', + 'mon' => 'M', + 'mm' => 'm', + 'm' => 'n', + 'ww' => 'W', + 'date' => 'N', + 'day' => 'l', + 'ddd' => 'D', + 'dd' => 'd', + 'd' => 'j', + ); + foreach ($formats as $token_format => $date_format) { + $tokens[$old_token . $token_format] = "$new_token:custom:$date_format"; + } + $tokens[$old_token . 'raw'] = "$new_token:raw"; + $tokens[$old_token . 'since'] = "$new_token:since"; + return $tokens; +} + +/** + * Update a string containing Drupal 6 style tokens to Drupal 7 style tokens. + * + * @param $text + * A string containing tokens. + * @param $updates + * An optional array of Drupal 7 tokens keyed by their Drupal 6 token name. + * The default tokens will be merged into this array. Note neither the old + * or new token names should include the surrounding bracket ([ and ]) + * characters. + * @return + * A string with the tokens upgraded + * + * @see _token_upgrade_token_list() + */ +function token_update_token_text($text, $updates = array(), $leading = '[', $trailing = ']') { + $updates += _token_upgrade_token_list(); + $regex = '/' . preg_quote($leading, '/') . '([^\s]*)' . preg_quote($trailing, '/') . '/'; + preg_match_all($regex, $text, $matches); + + foreach ($matches[1] as $index => $old_token) { + if (isset($updates[$old_token])) { + $new_token = $updates[$old_token]; + $text = str_replace("{$leading}{$old_token}{$trailing}", "[$new_token]", $text); + // Also replace any tokens that have a -raw suffix. + $text = str_replace("{$leading}{$old_token}-raw{$trailing}", "[$new_token]", $text); + } + } + + return $text; +} + +/** + * Get token problems. + */ +function token_get_token_problems() { + // @todo Improve the duplicate checking to report which modules are the offenders. + //$token_info = array(); + //foreach (module_implements('token_info') as $module) { + // $module_token_info = module_invoke($module, 'token_info'); + // if (in_array($module, _token_core_supported_modules())) { + // $module .= '/token'; + // } + // if (isset($module_token_info['types'])) { + // if (is_array($module_token_info['types'])) { + // foreach (array_keys($module_token_info['types']) as $type) { + // if (is_array($module_token_info['types'][$type])) { + // $module_token_info['types'][$type] += array('module' => $module); + // } + // } + // } + // } + // if (isset($module_token_info['tokens'])) { + // if (is_array($module_token_info['tokens'])) { + // + // } + // } + // if (is_array($module_token_info)) { + // $token_info = array_merge_recursive($token_info, $module_token_info); + // } + //} + + $token_info = \Drupal::token()->getInfo(); + $token_problems = array( + 'not-array' => array( + 'label' => t('Tokens or token types not defined as arrays'), + 'severity' => REQUIREMENT_ERROR, + ), + 'missing-info' => array( + 'label' => t('Tokens or token types missing name property'), + 'severity' => REQUIREMENT_WARNING, + ), + 'type-no-tokens' => array( + 'label' => t('Token types do not have any tokens defined'), + 'severity' => REQUIREMENT_INFO, + ), + 'tokens-no-type' => array( + 'label' => t('Token types are not defined but have tokens'), + 'severity' => REQUIREMENT_INFO, + ), + 'duplicate' => array( + 'label' => t('Token or token types are defined by multiple modules'), + 'severity' => REQUIREMENT_ERROR, + ), + ); + + // Check token types for problems. + foreach ($token_info['types'] as $type => $type_info) { + $real_type = !empty($type_info['type']) ? $type_info['type'] : $type; + if (!is_array($type_info)) { + $token_problems['not-array']['problems'][] = "\$info['types']['$type']"; + continue; + } + elseif (!isset($type_info['name'])) { + $token_problems['missing-info']['problems'][] = "\$info['types']['$type']"; + } + elseif (is_array($type_info['name'])) { + $token_problems['duplicate']['problems'][] = "\$info['types']['$type']"; + } + elseif (empty($token_info['tokens'][$real_type])) { + $token_problems['type-no-tokens']['problems'][] = "\$info['types']['$real_type']"; + } + } + + // Check tokens for problems. + foreach ($token_info['tokens'] as $type => $tokens) { + if (!is_array($tokens)) { + $token_problems['not-array']['problems'][] = "\$info['tokens']['$type']"; + continue; + } + else { + foreach (array_keys($tokens) as $token) { + if (!is_array($tokens[$token])) { + $token_problems['not-array']['problems'][] = "\$info['tokens']['$type']['$token']"; + continue; + } + elseif (!isset($tokens[$token]['name'])) { + $token_problems['missing-info']['problems'][] = "\$info['tokens']['$type']['$token']"; + } + elseif (is_array($tokens[$token]['name'])) { + $token_problems['duplicate']['problems'][] = "\$info['tokens']['$type']['$token']"; + } + } + } + if (!isset($token_info['types'][$type])) { + $token_problems['tokens-no-type']['problems'][] = "\$info['types']['$type']"; + } + } + + return $token_problems; +} \ No newline at end of file diff --git a/web/modules/contrib/token/token.libraries.yml b/web/modules/contrib/token/token.libraries.yml new file mode 100644 index 000000000..5c7ca14d1 --- /dev/null +++ b/web/modules/contrib/token/token.libraries.yml @@ -0,0 +1,25 @@ +jquery.treeTable: + remote: 'http://plugins.jquery.com/treetable/' + version: 3.2.0 + license: + name: MIT + url: https://github.com/ludo/jquery-treetable/blob/3.2.0/MIT-LICENSE.txt + gpl-compatible: true + js: + js/jquery.treetable.js: {} + css: + component: + css/jquery.treetable.css: {} + css/token.treetable.theme.css: {} + dependencies: + - core/jquery +token: + version: VERSION + js: + js/token.js: {} + css: + component: + css/token.css: {} + dependencies: + - core/jquery + - core/drupal diff --git a/web/modules/contrib/token/token.links.task.yml b/web/modules/contrib/token/token.links.task.yml new file mode 100644 index 000000000..e89ce1624 --- /dev/null +++ b/web/modules/contrib/token/token.links.task.yml @@ -0,0 +1,3 @@ +token.devel_entities: + class: \Drupal\Core\Menu\LocalTaskDefault + deriver: \Drupal\token\Plugin\Derivative\DevelLocalTask diff --git a/web/modules/contrib/token/token.module b/web/modules/contrib/token/token.module new file mode 100644 index 000000000..32a38da64 --- /dev/null +++ b/web/modules/contrib/token/token.module @@ -0,0 +1,788 @@ +buildAllRenderable([ + 'click_insert' => FALSE, + 'show_restricted' => TRUE, + 'show_nested' => FALSE, + ]); + $output = '

' . t('About') . '

'; + $output .= '

' . t('The Token module provides a user interface for the site token system. It also adds some additional tokens that are used extensively during site development. Tokens are specially formatted chunks of text that serve as placeholders for a dynamically generated value. For more information, covering both the token system and the additional tools provided by the Token module, see the online documentation.', [':online' => 'https://www.drupal.org/documentation/modules/token', ':project' => 'https://www.drupal.org/project/token']) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '

' . t('Your website uses a shared token system for exposing and using placeholder tokens and their appropriate replacement values. This allows for any module to provide placeholder tokens for strings without having to reinvent the wheel. It also ensures consistency in the syntax used for tokens, making the system as a whole easier for end users to use.') . '

'; + $output .= '
'; + $output .= '
' . t('The list of the currently available tokens on this site are shown below.') . '
'; + $output .= '
' . \Drupal::service('renderer')->render($token_tree) . '
'; + $output .= '
'; + return $output; + } +} + +/** + * Return an array of the core modules supported by token.module. + */ +function _token_core_supported_modules() { + return array('book', 'field', 'menu_ui'); +} + +/** + * Implements hook_theme(). + */ +function token_theme() { + $info['token_tree_link'] = [ + 'variables' => [ + 'token_types' => [], + 'global_types' => TRUE, + 'click_insert' => TRUE, + 'show_restricted' => FALSE, + 'show_nested' => FALSE, + 'recursion_limit' => 3, + 'text' => NULL, + 'options' => [], + ], + 'file' => 'token.pages.inc', + ]; + + return $info; +} + +/** + * Implements hook_block_view_alter(). + */ +function token_block_view_alter(&$build, BlockPluginInterface $block) { + $label = $build['#configuration']['label']; + if ($label != '') { + // The label is automatically escaped, avoid escaping it twice. + // @todo https://www.drupal.org/node/2580723 will add a method or option + // to the token API to do this, use that when available. + $bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build); + $build['#configuration']['label'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($label, [], [], $bubbleable_metadata)); + $bubbleable_metadata->applyTo($build); + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function token_form_block_form_alter(&$form, FormStateInterface $form_state) { + $token_tree = [ + '#theme' => 'token_tree_link', + '#token_types' => [], + ]; + $rendered_token_tree = \Drupal::service('renderer')->render($token_tree); + $form['settings']['label']['#description'] = + t('This field supports tokens. @browse_tokens_link', ['@browse_tokens_link' => $rendered_token_tree]) + ; + $form['settings']['label']['#element_validate'][] = 'token_element_validate'; + $form['settings']['label'] += ['#token_types' => []]; +} + +/** + * Implements hook_field_info_alter(). + */ +function token_field_info_alter(&$info) { + $defaults = array( + 'taxonomy_term_reference' => 'taxonomy_term_reference_plain', + 'number_integer' => 'number_unformatted', + 'number_decimal' => 'number_unformatted', + 'number_float' => 'number_unformatted', + 'file' => 'file_url_plain', + 'image' => 'file_url_plain', + 'text' => 'text_default', + 'text_long' => 'text_default', + 'text_with_summary' => 'text_default', + 'list_integer' => 'list_default', + 'list_float' => 'list_default', + 'list_string' => 'list_default', + 'list_boolean' => 'list_default', + ); + foreach ($defaults as $field_type => $default_token_formatter) { + if (isset($info[$field_type])) { + $info[$field_type] += array('default_token_formatter' => $default_token_formatter); + } + } +} + +/** + * Implements hook_date_format_insert(). + */ +function token_date_format_insert() { + token_clear_cache(); +} + +/** + * Implements hook_date_format_delete(). + */ +function token_date_format_delete() { + token_clear_cache(); +} + +/** + * Implements hook_field_storage_config_presave(). + */ +function token_field_config_presave($instance) { + token_clear_cache(); +} + +/** + * Implements hook_field_storage_config_delete(). + */ +function token_field_config_delete($instance) { + token_clear_cache(); +} + +/** + * Clear token caches and static variables. + */ +function token_clear_cache() { + \Drupal::token()->resetInfo(); + \Drupal::service('token.entity_mapper')->resetInfo(); + drupal_static_reset('token_menu_link_load_all_parents'); + drupal_static_reset('token_book_link_load'); +} + +/** + * Implements hook_entity_type_alter(). + * + * Because some token types to do not match their entity type names, we have to + * map them to the proper type. This is purely for other modules' benefit. + * + * @see \Drupal\token\TokenEntityMapperInterface::getEntityTypeMappings() + * @see http://drupal.org/node/737726 + */ +function token_entity_type_alter(array &$entity_types) { + $devel_exists = \Drupal::moduleHandler()->moduleExists('devel'); + /* @var $entity_types EntityTypeInterface[] */ + foreach ($entity_types as $entity_type_id => $entity_type) { + if (!$entity_type->get('token_type')) { + // Fill in default token types for entities. + switch ($entity_type_id) { + case 'taxonomy_term': + case 'taxonomy_vocabulary': + // Stupid taxonomy token types... + $entity_type->set('token_type', str_replace('taxonomy_', '', $entity_type_id)); + break; + + default: + // By default the token type is the same as the entity type. + $entity_type->set('token_type', $entity_type_id); + break; + } + } + + if ($devel_exists + && $entity_type->hasViewBuilderClass() + && ($canonical = $entity_type->getLinkTemplate('canonical')) + && !$entity_type->hasLinkTemplate('token-devel')) { + $entity_type->setLinkTemplate('token-devel', $canonical . '/devel/token'); + } + } +} + +/** + * Implements hook_entity_view_modes_info(). + */ + +/** + * Implements hook_module_implements_alter(). + * + * Adds missing token support for core modules. + */ +function token_module_implements_alter(&$implementations, $hook) { + module_load_include('inc', 'token', 'token.tokens'); + + if ($hook == 'tokens' || $hook == 'token_info' || $hook == 'token_info_alter' || $hook == 'tokens_alter') { + foreach (_token_core_supported_modules() as $module) { + if (\Drupal::moduleHandler()->moduleExists($module) && function_exists($module . '_' . $hook)) { + $implementations[$module] = TRUE; + } + } + // Move token.module to get included first since it is responsible for + // other modules. + if (isset($implementations['token'])) { + unset($implementations['token']); + $implementations = array_merge(array('token' => 'tokens'), $implementations); + } + } +} + +/** + * Return the module responsible for a token. + * + * @param string $type + * The token type. + * @param string $name + * The token name. + * + * @return mixed + * The value of $info['tokens'][$type][$name]['module'] from token info, or + * NULL if the value does not exist. + */ +function _token_module($type, $name) { + $token_info = \Drupal::token()->getTokenInfo($type, $name); + return isset($token_info['module']) ? $token_info['module'] : NULL; +} + +/** + * Validate a form element that should have tokens in it. + * + * Form elements that want to add this validation should have the #token_types + * parameter defined. + * + * For example: + * @code + * $form['my_node_text_element'] = array( + * '#type' => 'textfield', + * '#title' => t('Some text to token-ize that has a node context.'), + * '#default_value' => 'The title of this node is [node:title].', + * '#element_validate' => array('token_element_validate'), + * '#token_types' => array('node'), + * '#min_tokens' => 1, + * '#max_tokens' => 10, + * ); + * @endcode + */ +function token_element_validate($element, FormStateInterface $form_state) { + $value = isset($element['#value']) ? $element['#value'] : $element['#default_value']; + + if (!Unicode::strlen($value)) { + // Empty value needs no further validation since the element should depend + // on using the '#required' FAPI property. + return $element; + } + + $tokens = \Drupal::token()->scan($value); + $title = empty($element['#title']) ? $element['#parents'][0] : $element['#title']; + + // Validate if an element must have a minimum number of tokens. + if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) { + $error = \Drupal::translation()->formatPlural($element['#min_tokens'], '%name must contain at least one token.', '%name must contain at least @count tokens.', array('%name' => $title)); + $form_state->setError($element, $error); + } + + // Validate if an element must have a maximum number of tokens. + if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) { + $error = \Drupal::translation()->formatPlural($element['#max_tokens'], '%name must contain at most one token.', '%name must contain at most @count tokens.', array('%name' => $title)); + $form_state->setError($element, $error); + } + + // Check if the field defines specific token types. + if (isset($element['#token_types'])) { + $invalid_tokens = \Drupal::token()->getInvalidTokensByContext($tokens, $element['#token_types']); + if ($invalid_tokens) { + $form_state->setError($element, t('%name is using the following invalid tokens: @invalid-tokens.', array('%name' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens)))); + } + } + + return $element; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function token_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) { + $field_config = $form_state->getFormObject()->getEntity(); + $field_storage = $field_config->getFieldStorageDefinition(); + if ($field_storage->isLocked()) { + return; + } + $field_type = $field_storage->getType(); + if (($field_type == 'file' || $field_type == 'image') && isset($form['settings']['file_directory'])) { + // GAH! We can only support global tokens in the upload file directory path. + $form['settings']['file_directory']['#element_validate'][] = 'token_element_validate'; + // Date support needs to be implicitly added, as while technically it's not + // a global token, it is a not only used but is the default value. + // https://www.drupal.org/node/2642160 + $form['settings']['file_directory'] += array('#token_types' => array('date')); + $form['settings']['file_directory']['#description'] .= ' ' . t('This field supports tokens.'); + } + + // Note that the description is tokenized via token_field_widget_form_alter(). + $form['description']['#element_validate'][] = 'token_element_validate'; + $form['description'] += array('#token_types' => array()); + + $form['token_tree'] = array( + '#theme' => 'token_tree_link', + '#token_types' => array(), + '#weight' => $form['description']['#weight'] + 0.5, + ); +} + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + * + * Alters the configure action form to add token context validation and + * adds the token tree for a better token UI and selection. + */ +function token_form_action_form_alter(&$form, $form_state) { + switch ($form['plugin']['#value']) { + case 'action_message_action': + case 'action_send_email_action': + case 'action_goto_action': + $form['token_tree'] = [ + '#theme' => 'token_tree_link', + '#token_types' => 'all', + '#weight' => 100, + ]; + $form['actions']['#weight'] = 101; + // @todo Add token validation to the action fields that can use tokens. + break; + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Alters the user e-mail fields to add token context validation and + * adds the token tree for a better token UI and selection. + */ +function token_form_user_admin_settings_alter(&$form, FormStateInterface $form_state) { + $email_token_help = t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].'); + + foreach (Element::children($form) as $key) { + $element = &$form[$key]; + + // Remove the crummy default token help text. + if (!empty($element['#description'])) { + $element['#description'] = trim(str_replace($email_token_help, t('The list of available tokens that can be used in e-mails is provided below.'), $element['#description'])); + } + + switch ($key) { + case 'email_admin_created': + case 'email_pending_approval': + case 'email_no_approval_required': + case 'email_password_reset': + case 'email_cancel_confirm': + // Do nothing, but allow execution to continue. + break; + case 'email_activated': + case 'email_blocked': + case 'email_canceled': + // These fieldsets have their e-mail elements inside a 'settings' + // sub-element, so switch to that element instead. + $element = &$form[$key]['settings']; + break; + default: + continue 2; + } + + foreach (Element::children($element) as $sub_key) { + if (!isset($element[$sub_key]['#type'])) { + continue; + } + elseif ($element[$sub_key]['#type'] == 'textfield' && substr($sub_key, -8) === '_subject') { + // Add validation to subject textfields. + $element[$sub_key]['#element_validate'][] = 'token_element_validate'; + $element[$sub_key] += array('#token_types' => array('user')); + } + elseif ($element[$sub_key]['#type'] == 'textarea' && substr($sub_key, -5) === '_body') { + // Add validation to body textareas. + $element[$sub_key]['#element_validate'][] = 'token_element_validate'; + $element[$sub_key] += array('#token_types' => array('user')); + } + } + } + + // Add the token tree UI. + $form['email']['token_tree'] = array( + '#theme' => 'token_tree_link', + '#token_types' => array('user'), + '#show_restricted' => TRUE, + '#show_nested' => FALSE, + '#weight' => 90, + ); +} + +/** + * Prepare a string for use as a valid token name. + * + * @param $name + * The token name to clean. + * @return + * The cleaned token name. + */ +function token_clean_token_name($name) { + static $names = array(); + + if (!isset($names[$name])) { + $cleaned_name = strtr($name, array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '')); + $cleaned_name = preg_replace('/[^\w\-]/i', '', $cleaned_name); + $cleaned_name = trim($cleaned_name, '-'); + $names[$name] = $cleaned_name; + } + + return $names[$name]; +} + +/** + * Do not use this function yet. Its API has not been finalized. + */ +function token_render_array(array $array, array $options = array()) { + $rendered = array(); + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + foreach (token_element_children($array) as $key) { + $value = $array[$key]; + $rendered[] = is_array($value) ? $renderer->renderPlain($value) : (string) $value; + } + $join = isset($options['join']) ? $options['join'] : ', '; + return implode($join, $rendered); +} + +/** + * Do not use this function yet. Its API has not been finalized. + */ +function token_render_array_value($value, array $options = array()) { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + $rendered = is_array($value) ? $renderer->renderPlain($value) : (string) $value; + return $rendered; +} + +/** + * Copy of drupal_render_cache_get() that does not care about request method. + */ +function token_render_cache_get($elements) { + if (!$cid = drupal_render_cid_create($elements)) { + return FALSE; + } + $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render'; + + if (!empty($cid) && $cache = \Drupal::cache($bin)->get($cid)) { + // Add additional libraries, JavaScript, CSS and other data attached + // to this element. + if (isset($cache->data['#attached'])) { + drupal_process_attached($cache->data); + } + // Return the rendered output. + return $cache->data['#markup']; + } + return FALSE; +} + +/** + * Coyp of drupal_render_cache_set() that does not care about request method. + */ +function token_render_cache_set(&$markup, $elements) { + // This should only run of drupal_render_cache_set() did not. + if (in_array(\Drupal::request()->server->get('REQUEST_METHOD'), array('GET', 'HEAD'))) { + return FALSE; + } + + $original_method = \Drupal::request()->server->get('REQUEST_METHOD'); + \Drupal::request()->server->set('REQUEST_METHOD', 'GET'); + drupal_render_cache_set($markup, $elements); + \Drupal::request()->server->set('REQUEST_METHOD', $original_method); +} + +/** + * Loads menu link titles for all purents of a menu link plugin ID. + * + * @param string $plugin_id + * The menu link plugin ID. + * @param string $langcode + * The language code. + * + * @return string[] + * List of menu link parent titles. + */ +function token_menu_link_load_all_parents($plugin_id, $langcode) { + $cache = &drupal_static(__FUNCTION__, array()); + + if (!isset($cache[$plugin_id][$langcode])) { + $cache[$plugin_id][$langcode] = array(); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $parent_ids = $menu_link_manager->getParentIds($plugin_id); + // Remove the current plugin ID from the parents. + unset($parent_ids[$plugin_id]); + foreach ($parent_ids as $parent_id) { + $parent = $menu_link_manager->createInstance($parent_id); + $cache[$plugin_id][$langcode] = array($parent_id => token_menu_link_translated_title($parent, $langcode)) + $cache[$plugin_id][$langcode]; + } + } + + return $cache[$plugin_id][$langcode]; +} + +/** + * Returns the translated link of a menu title. + * + * If the underlying entity is a content menu item, load it to get the + * translated menu item title. + * + * @todo Remove this when there is a better way to get a translated menu + * item title in core: https://www.drupal.org/node/2795143 + * + * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link + * The menu link. + * @param string|null $langcode + * (optional) The langcode, defaults to the current language. + * + * @return string + * The menu link title. + */ +function token_menu_link_translated_title(MenuLinkInterface $menu_link, $langcode = NULL) { + $metadata = $menu_link->getMetaData(); + if (isset($metadata['entity_id']) && $menu_link->getProvider() == 'menu_link_content') { + /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */ + $entity = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load($metadata['entity_id']); + $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode); + return $entity->getTitle(); + } + return $menu_link->getTitle(); +} + +/** + * Loads all the parents of the term in the specified language. + * + * @param int $tid + * The term id. + * @param string $langcode + * The language code. + * + * @return string[] + * The term parents collection. + */ +function token_taxonomy_term_load_all_parents($tid, $langcode) { + $cache = &drupal_static(__FUNCTION__, array()); + + if (!is_numeric($tid)) { + return array(); + } + + if (!isset($cache[$langcode][$tid])) { + $cache[$langcode][$tid] = array(); + /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */ + $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); + $parents = $term_storage->loadAllParents($tid); + // Remove this term from the array. + array_shift($parents); + $parents = array_reverse($parents); + foreach ($parents as $term) { + $translation = \Drupal::service('entity.repository')->getTranslationFromContext($term, $langcode); + $cache[$langcode][$tid][$term->id()] = $translation->label(); + } + } + + return $cache[$langcode][$tid]; +} + +function token_element_children(&$elements, $sort = FALSE) { + // Do not attempt to sort elements which have already been sorted. + $sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort; + + // Filter out properties from the element, leaving only children. + $children = array(); + $sortable = FALSE; + foreach ($elements as $key => $value) { + if ($key === '' || $key[0] !== '#') { + $children[$key] = $value; + if (is_array($value) && isset($value['#weight'])) { + $sortable = TRUE; + } + } + } + // Sort the children if necessary. + if ($sort && $sortable) { + uasort($children, 'Drupal\Component\Utility\SortArray::sortByWeightProperty'); + // Put the sorted children back into $elements in the correct order, to + // preserve sorting if the same element is passed through + // element_children() twice. + foreach ($children as $key => $child) { + unset($elements[$key]); + $elements[$key] = $child; + } + $elements['#sorted'] = TRUE; + } + + return array_keys($children); +} + +/** + * Loads all the parents of the book page. + * + * @param array $book + * The book data. The 'nid' key points to the current page of the book. + * The 'p1' ... 'p9' keys point to parents of the page, if they exist, with 'p1' + * pointing to the book itself and the last defined pX to the current page. + * + * @return string[] + * List of node titles of the book parents. + */ +function token_book_load_all_parents(array $book) { + $cache = &drupal_static(__FUNCTION__, array()); + + if (empty($book['nid'])) { + return array(); + } + $nid = $book['nid']; + + if (!isset($cache[$nid])) { + $cache[$nid] = array(); + $i = 1; + while ($book["p$i"] != $nid) { + $cache[$nid][] = Node::load($book["p$i"])->getTitle(); + $i++; + } + } + + return $cache[$nid]; +} + +/** + * Implements hook_entity_base_field_info(). + */ +function token_entity_base_field_info(EntityTypeInterface $entity_type) { + // We add a psuedo entity-reference field to track the menu entry created + // from the node add/edit form so that tokens generated at that time that + // reference the menu link can access the yet to be saved menu link. + // @todo Revisit when https://www.drupal.org/node/2315773 is resolved. + if ($entity_type->id() === 'node' && \Drupal::moduleHandler()->moduleExists('menu_ui')) { + $fields['menu_link'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Menu link')) + ->setDescription(t('Computed menu link for the node (only available during node saving).')) + ->setRevisionable(TRUE) + ->setSetting('target_type', 'menu_link_content') + ->setTranslatable(TRUE) + ->setDisplayOptions('view', array( + 'label' => 'hidden', + 'type' => 'hidden', + )) + ->setComputed(TRUE) + ->setDisplayOptions('form', array( + 'type' => 'hidden', + )); + + return $fields; + } + return []; +} + +/** + * Implements hook_form_BASE_FORM_ID_alter() for node_form. + * + * Populates menu_link field on nodes from the menu item on unsaved nodes. + * + * @see menu_ui_form_node_form_submit() + * @see token_entity_base_field_info() + */ +function token_form_node_form_alter(&$form, FormStateInterface $form_state) { + if (!\Drupal::moduleHandler()->moduleExists('menu_ui')) { + return; + } + /** @var \Drupal\node\NodeForm $form_object */ + if (!\Drupal::currentUser()->hasPermission('administer menu')) { + // We're only interested in when the node is unsaved and the editor has + // permission to create new menu links. + return; + } + $form['#entity_builders'][] = 'token_node_menu_link_submit'; +} + +/** + * Entity builder. + */ +function token_node_menu_link_submit($entity_type, NodeInterface $node, &$form, FormStateInterface $form_state) { + // Entity builders run twice, once during validation and again during + // submission, so we only run this code after validation has been performed. + if (!$form_state->isValueEmpty('menu') && $form_state->getTemporaryValue('entity_validated')) { + $values = $form_state->getValue('menu'); + if (!empty($values['enabled']) && trim($values['title'])) { + if (!empty($values['menu_parent'])) { + list($menu_name, $parent) = explode(':', $values['menu_parent'], 2); + $values['menu_name'] = $menu_name; + $values['parent'] = $parent; + } + // Construct an unsaved entity. + if ($entity_id = $form_state->getValue(['menu', 'entity_id'])) { + // Use the existing menu_link_content entity. + $entity = MenuLinkContent::load($entity_id); + // If the loaded MenuLinkContent doesn't have a translation for the + // Node's active langcode, create a new translation. + if ($entity->isTranslatable()) { + if (!$entity->hasTranslation($node->language()->getId())) { + $entity = $entity->addTranslation($node->language()->getId(), $entity->toArray()); + } + else { + $entity = $entity->getTranslation($node->language()->getId()); + } + } + } + else { + if ($node->isNew()) { + // Create a new menu_link_content entity. + $entity = MenuLinkContent::create(array( + // Lets just reference the UUID for now, the link is not important for + // token generation. + 'link' => ['uri' => 'internal:/node/' . $node->uuid()], + 'langcode' => $node->language()->getId(), + )); + } + else { + // Create a new menu_link_content entity. + $entity = MenuLinkContent::create(array( + 'link' => ['uri' => 'entity:node/' . $node->id()], + 'langcode' => $node->language()->getId(), + )); + } + } + $entity->title->value = trim($values['title']); + $entity->description->value = trim($values['description']); + $entity->menu_name->value = $values['menu_name']; + $entity->parent->value = $values['parent']; + $entity->weight->value = isset($values['weight']) ? $values['weight'] : 0; + $entity->save(); + $node->menu_link = $entity; + // Leave this for _menu_ui_node_save() to pick up so we don't end up with + // duplicate menu-links. + $form_state->setValue(['menu', 'entity_id'], $entity->id()); + } + } +} + +/** + * Implements hook_ENTITY_TYPE_insert for node entities. + */ +function token_node_insert(NodeInterface $node) { + if ($node->hasField('menu_link') && $menu_link = $node->menu_link->entity) { + // Update the menu-link to point to the now saved node. + $menu_link->link = 'entity:node/' . $node->id(); + $menu_link->save(); + } +} + +/** + * Implements hook_ENTITY_TYPE_presave() for menu_link_content. + */ +function token_menu_link_content_presave(MenuLinkContentInterface $menu_link_content) { + drupal_static_reset('token_menu_link_load_all_parents'); +} diff --git a/web/modules/contrib/token/token.pages.inc b/web/modules/contrib/token/token.pages.inc new file mode 100644 index 000000000..4b92df1f1 --- /dev/null +++ b/web/modules/contrib/token/token.pages.inc @@ -0,0 +1,61 @@ + [], + 'global_types' => TRUE, + 'click_insert' => TRUE, + 'show_restricted' => FALSE, + 'show_nested' => FALSE, + 'recursion_limit' => 3, + ]; + $query_options = array_intersect_key($variables, $tree_variables); + $query_options = DiffArray::diffAssocRecursive($query_options, $tree_variables); + if (!isset($variables['options']['query']['options'])) { + $variables['options']['query']['options'] = []; + } + $variables['options']['query']['options'] += $query_options; + + // Because PHP converts query strings with arrays into a different syntax on + // the next request, the options have to be encoded with JSON in the query + // string so that we can reliably decode it for token comparison. + $variables['options']['query']['options'] = Json::encode($variables['options']['query']['options']); + + // Set the token tree to open in a separate window. + $variables['options']['attributes'] += [ + 'data-dialog-type' => 'dialog', + 'data-dialog-options' => json_encode([ + 'dialogClass' => 'token-tree-dialog', + 'width' => 600, + 'height' => 400, + 'position' => ['my' => 'right bottom', 'at' => 'right-10 bottom-10'], + 'draggable' => TRUE, + 'autoResize' => FALSE, + ]), + ]; + + $variables['link'] = Link::createFromRoute($variables['text'], 'token.tree', [], $variables['options'])->toRenderable(); + $variables['url'] = new Url('token.tree', [], $variables['options']); + $variables['attributes'] = $variables['options']['attributes']; +} diff --git a/web/modules/contrib/token/token.routing.yml b/web/modules/contrib/token/token.routing.yml new file mode 100644 index 000000000..f2105aeaf --- /dev/null +++ b/web/modules/contrib/token/token.routing.yml @@ -0,0 +1,22 @@ +token.tree: + path: '/token/tree' + defaults: + _controller: '\Drupal\token\Controller\TokenTreeController::outputTree' + requirements: + _csrf_token: 'TRUE' + +token.autocomplete: + path: '/token/autocomplete/{token_type}/{filter}' + defaults: + _controller: '\Drupal\token\Controller\TokenAutocompleteController::autocomplete' + requirements: + _access: 'TRUE' + +token.flush_cache: + path: '/token/flush-cache' + defaults: + _controller: '\Drupal\token\Controller\TokenCacheController::flush' + requirements: + _permission: 'flush caches' + _csrf_token: 'TRUE' + _module_dependencies: 'admin_menu' diff --git a/web/modules/contrib/token/token.services.yml b/web/modules/contrib/token/token.services.yml new file mode 100644 index 000000000..f6011aa6b --- /dev/null +++ b/web/modules/contrib/token/token.services.yml @@ -0,0 +1,12 @@ +services: + token.entity_mapper: + class: Drupal\token\TokenEntityMapper + arguments: ['@entity_type.manager', '@module_handler'] + token.tree_builder: + class: Drupal\token\TreeBuilder + arguments: ['@token', '@token.entity_mapper', '@cache.data', '@language_manager'] + token.route_subscriber: + class: Drupal\token\Routing\RouteSubscriber + arguments: ['@entity_type.manager', '@module_handler'] + tags: + - { name: event_subscriber } diff --git a/web/modules/contrib/token/token.tokens.inc b/web/modules/contrib/token/token.tokens.inc new file mode 100644 index 000000000..f6ad61910 --- /dev/null +++ b/web/modules/contrib/token/token.tokens.inc @@ -0,0 +1,1721 @@ + t('Current date'), + 'description' => t('Tokens related to the current date and time.'), + 'type' => 'date', + ); + + // Add a 'dynamic' key to any tokens that have chained but dynamic tokens. + $info['tokens']['date']['custom']['dynamic'] = TRUE; + + // The [file:size] may not always return in kilobytes. + // @todo Remove when http://drupal.org/node/1193044 is fixed. + if (!empty($info['tokens']['file']['size'])) { + $info['tokens']['file']['size']['description'] = t('The size of the file.'); + } + + // Remove deprecated tokens from being listed. + unset($info['tokens']['node']['tnid']); + unset($info['tokens']['node']['type']); + unset($info['tokens']['node']['type-name']); + + // Support 'url' type tokens for core tokens. + if (isset($info['tokens']['comment']['url']) && \Drupal::moduleHandler()->moduleExists('comment')) { + $info['tokens']['comment']['url']['type'] = 'url'; + } + if (isset($info['tokens']['node']['url']) && \Drupal::moduleHandler()->moduleExists('node')) { + $info['tokens']['node']['url']['type'] = 'url'; + } + if (isset($info['tokens']['term']['url']) && \Drupal::moduleHandler()->moduleExists('taxonomy')) { + $info['tokens']['term']['url']['type'] = 'url'; + } + $info['tokens']['user']['url']['type'] = 'url'; + + // Add [token:url] tokens for any URI-able entities. + $entities = \Drupal::entityTypeManager()->getDefinitions(); + foreach ($entities as $entity => $entity_info) { + // Do not generate tokens if the entity doesn't define a token type or is + // not a content entity. + if (!$entity_info->get('token_type') || (!$entity_info instanceof ContentEntityTypeInterface)) { + continue; + } + + $token_type = $entity_info->get('token_type'); + if (!isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) { + // Define tokens for entity type's without their own integration. + $info['types'][$entity_info->id()] = [ + 'name' => $entity_info->getLabel(), + 'needs-data' => $entity_info->id(), + 'module' => 'token', + ]; + } + + // Add [entity:url] tokens if they do not already exist. + // @todo Support entity:label + if (!isset($info['tokens'][$token_type]['url'])) { + $info['tokens'][$token_type]['url'] = array( + 'name' => t('URL'), + 'description' => t('The URL of the @entity.', array('@entity' => Unicode::strtolower($entity_info->getLabel()))), + 'module' => 'token', + 'type' => 'url', + ); + } + + // Add [entity:original] tokens if they do not already exist. + if (!isset($info['tokens'][$token_type]['original'])) { + $info['tokens'][$token_type]['original'] = array( + 'name' => t('Original @entity', array('@entity' => Unicode::strtolower($entity_info->getLabel()))), + 'description' => t('The original @entity data if the @entity is being updated or saved.', array('@entity' => Unicode::strtolower($entity_info->getLabel()))), + 'module' => 'token', + 'type' => $token_type, + ); + } + } + + // Add support for custom date formats. + // @todo Remove when http://drupal.org/node/1173706 is fixed. + $date_format_types = \Drupal::entityTypeManager()->getStorage('date_format')->loadMultiple(); + foreach ($date_format_types as $date_format_type => $date_format_type_info) { + /* @var \Drupal\system\Entity\DateFormat $date_format_type_info */ + if (!isset($info['tokens']['date'][$date_format_type])) { + $info['tokens']['date'][$date_format_type] = array( + 'name' => Html::escape($date_format_type_info->label()), + 'description' => t("A date in '@type' format. (%date)", array('@type' => $date_format_type, '%date' => format_date(REQUEST_TIME, $date_format_type))), + 'module' => 'token', + ); + } + } +} + +/** + * Implements hook_token_info(). + */ +function token_token_info() { + // Node tokens. + $info['tokens']['node']['source'] = array( + 'name' => t('Translation source node'), + 'description' => t("The source node for this current node's translation set."), + 'type' => 'node', + ); + $info['tokens']['node']['log'] = array( + 'name' => t('Revision log message'), + 'description' => t('The explanation of the most recent changes made to the node.'), + ); + $info['tokens']['node']['content-type'] = array( + 'name' => t('Content type'), + 'description' => t('The content type of the node.'), + 'type' => 'content-type', + ); + + // Content type tokens. + $info['types']['content-type'] = array( + 'name' => t('Content types'), + 'description' => t('Tokens related to content types.'), + 'needs-data' => 'node_type', + ); + $info['tokens']['content-type']['name'] = array( + 'name' => t('Name'), + 'description' => t('The name of the content type.'), + ); + $info['tokens']['content-type']['machine-name'] = array( + 'name' => t('Machine-readable name'), + 'description' => t('The unique machine-readable name of the content type.'), + ); + $info['tokens']['content-type']['description'] = array( + 'name' => t('Description'), + 'description' => t('The optional description of the content type.'), + ); + $info['tokens']['content-type']['node-count'] = array( + 'name' => t('Node count'), + 'description' => t('The number of nodes belonging to the content type.'), + ); + $info['tokens']['content-type']['edit-url'] = array( + 'name' => t('Edit URL'), + 'description' => t("The URL of the content type's edit page."), + // 'type' => 'url', + ); + + // Taxonomy term and vocabulary tokens. + if (\Drupal::moduleHandler()->moduleExists('taxonomy')) { + $info['tokens']['term']['edit-url'] = array( + 'name' => t('Edit URL'), + 'description' => t("The URL of the taxonomy term's edit page."), + // 'type' => 'url', + ); + $info['tokens']['term']['parents'] = array( + 'name' => t('Parents'), + 'description' => t("An array of all the term's parents, starting with the root."), + 'type' => 'array', + ); + $info['tokens']['term']['root'] = array( + 'name' => t('Root term'), + 'description' => t("The root term of the taxonomy term."), + 'type' => 'term', + ); + + $info['tokens']['vocabulary']['machine-name'] = array( + 'name' => t('Machine-readable name'), + 'description' => t('The unique machine-readable name of the vocabulary.'), + ); + $info['tokens']['vocabulary']['edit-url'] = array( + 'name' => t('Edit URL'), + 'description' => t("The URL of the vocabulary's edit page."), + // 'type' => 'url', + ); + } + + // File tokens. + $info['tokens']['file']['basename'] = array( + 'name' => t('Base name'), + 'description' => t('The base name of the file.'), + ); + $info['tokens']['file']['extension'] = array( + 'name' => t('Extension'), + 'description' => t('The extension of the file.'), + ); + $info['tokens']['file']['size-raw'] = array( + 'name' => t('File byte size'), + 'description' => t('The size of the file, in bytes.'), + ); + + // User tokens. + // Add information on the restricted user tokens. + $info['tokens']['user']['cancel-url'] = array( + 'name' => t('Account cancellation URL'), + 'description' => t('The URL of the confirm delete page for the user account.'), + 'restricted' => TRUE, + // 'type' => 'url', + ); + $info['tokens']['user']['one-time-login-url'] = array( + 'name' => t('One-time login URL'), + 'description' => t('The URL of the one-time login page for the user account.'), + 'restricted' => TRUE, + // 'type' => 'url', + ); + $info['tokens']['user']['roles'] = array( + 'name' => t('Roles'), + 'description' => t('The user roles associated with the user account.'), + 'type' => 'array', + ); + + // Current user tokens. + $info['tokens']['current-user']['ip-address'] = array( + 'name' => t('IP address'), + 'description' => 'The IP address of the current user.', + ); + + // Menu link tokens (work regardless if menu module is enabled or not). + $info['types']['menu-link'] = array( + 'name' => t('Menu links'), + 'description' => t('Tokens related to menu links.'), + 'needs-data' => 'menu-link', + ); + $info['tokens']['menu-link']['mlid'] = array( + 'name' => t('Link ID'), + 'description' => t('The unique ID of the menu link.'), + ); + $info['tokens']['menu-link']['title'] = array( + 'name' => t('Title'), + 'description' => t('The title of the menu link.'), + ); + $info['tokens']['menu-link']['url'] = array( + 'name' => t('URL'), + 'description' => t('The URL of the menu link.'), + 'type' => 'url', + ); + $info['tokens']['menu-link']['parent'] = array( + 'name' => t('Parent'), + 'description' => t("The menu link's parent."), + 'type' => 'menu-link', + ); + $info['tokens']['menu-link']['parents'] = array( + 'name' => t('Parents'), + 'description' => t("An array of all the menu link's parents, starting with the root."), + 'type' => 'array', + ); + $info['tokens']['menu-link']['root'] = array( + 'name' => t('Root'), + 'description' => t("The menu link's root."), + 'type' => 'menu-link', + ); + + // Current page tokens. + $info['types']['current-page'] = array( + 'name' => t('Current page'), + 'description' => t('Tokens related to the current page request.'), + ); + $info['tokens']['current-page']['title'] = array( + 'name' => t('Title'), + 'description' => t('The title of the current page.'), + ); + $info['tokens']['current-page']['url'] = array( + 'name' => t('URL'), + 'description' => t('The URL of the current page.'), + 'type' => 'url', + ); + $info['tokens']['current-page']['page-number'] = array( + 'name' => t('Page number'), + 'description' => t('The page number of the current page when viewing paged lists.'), + ); + $info['tokens']['current-page']['query'] = array( + 'name' => t('Query string value'), + 'description' => t('The value of a specific query string field of the current page.'), + 'dynamic' => TRUE, + ); + + // URL tokens. + $info['types']['url'] = array( + 'name' => t('URL'), + 'description' => t('Tokens related to URLs.'), + 'needs-data' => 'path', + ); + $info['tokens']['url']['path'] = array( + 'name' => t('Path'), + 'description' => t('The path component of the URL.'), + ); + $info['tokens']['url']['relative'] = array( + 'name' => t('Relative URL'), + 'description' => t('The relative URL.'), + ); + $info['tokens']['url']['absolute'] = array( + 'name' => t('Absolute URL'), + 'description' => t('The absolute URL.'), + ); + $info['tokens']['url']['brief'] = array( + 'name' => t('Brief URL'), + 'description' => t('The URL without the protocol and trailing backslash.'), + ); + $info['tokens']['url']['unaliased'] = array( + 'name' => t('Unaliased URL'), + 'description' => t('The unaliased URL.'), + 'type' => 'url', + ); + $info['tokens']['url']['args'] = array( + 'name' => t('Arguments'), + 'description' => t("The specific argument of the current page (e.g. 'arg:1' on the page 'node/1' returns '1')."), + 'type' => 'array', + ); + + // Array tokens. + $info['types']['array'] = array( + 'name' => t('Array'), + 'description' => t('Tokens related to arrays of strings.'), + 'needs-data' => 'array', + 'nested' => TRUE, + ); + $info['tokens']['array']['first'] = array( + 'name' => t('First'), + 'description' => t('The first element of the array.'), + ); + $info['tokens']['array']['last'] = array( + 'name' => t('Last'), + 'description' => t('The last element of the array.'), + ); + $info['tokens']['array']['count'] = array( + 'name' => t('Count'), + 'description' => t('The number of elements in the array.'), + ); + $info['tokens']['array']['reversed'] = array( + 'name' => t('Reversed'), + 'description' => t('The array reversed.'), + 'type' => 'array', + ); + $info['tokens']['array']['keys'] = array( + 'name' => t('Keys'), + 'description' => t('The array of keys of the array.'), + 'type' => 'array', + ); + $info['tokens']['array']['join'] = array( + 'name' => t('Imploded'), + 'description' => t('The values of the array joined together with a custom string in-between each value.'), + 'dynamic' => TRUE, + ); + $info['tokens']['array']['value'] = array( + 'name' => t('Value'), + 'description' => t('The specific value of the array.'), + 'dynamic' => TRUE, + ); + + // Random tokens. + $info['types']['random'] = array( + 'name' => t('Random'), + 'description' => t('Tokens related to random data.'), + ); + $info['tokens']['random']['number'] = array( + 'name' => t('Number'), + 'description' => t('A random number from 0 to @max.', array('@max' => mt_getrandmax())), + ); + $info['tokens']['random']['hash'] = array( + 'name' => t('Hash'), + 'description' => t('A random hash. The possible hashing algorithms are: @hash-algos.', array('@hash-algos' => implode(', ', hash_algos()))), + 'dynamic' => TRUE, + ); + + // Define image_with_image_style token type. + if (\Drupal::moduleHandler()->moduleExists('image')) { + $info['types']['image_with_image_style'] = [ + 'name' => t('Image with image style'), + 'needs-data' => 'image_with_image_style', + 'module' => 'token', + 'nested' => TRUE, + ]; + + // Provide tokens for the ImageStyle attributes. + $info['tokens']['image_with_image_style']['mimetype'] = [ + 'name' => t('MIME type'), + 'description' => t('The MIME type (image/png, image/bmp, etc.) of the image.'), + ]; + $info['tokens']['image_with_image_style']['filesize'] = [ + 'name' => t('File size'), + 'description' => t('The file size of the image.'), + ]; + $info['tokens']['image_with_image_style']['height'] = [ + 'name' => t('Height'), + 'description' => t('The height the image, in pixels.'), + ]; + $info['tokens']['image_with_image_style']['width'] = [ + 'name' => t('Width'), + 'description' => t('The width of the image, in pixels.'), + ]; + $info['tokens']['image_with_image_style']['uri'] = [ + 'name' => t('URI'), + 'description' => t('The URI to the image.'), + ]; + $info['tokens']['image_with_image_style']['url'] = [ + 'name' => t('URL'), + 'description' => t('The URL to the image.'), + ]; + } + + return $info; +} + +/** + * Implements hook_tokens(). + */ +function token_tokens($type, array $tokens, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata) { + $replacements = array(); + $language_manager = \Drupal::languageManager(); + $url_options = array('absolute' => TRUE); + if (isset($options['langcode'])) { + $url_options['language'] = $language_manager->getLanguage($options['langcode']); + $langcode = $options['langcode']; + } + else { + $langcode = $language_manager->getCurrentLanguage()->getId(); + } + + // Date tokens. + if ($type == 'date') { + $date = !empty($data['date']) ? $data['date'] : REQUEST_TIME; + + // @todo Remove when http://drupal.org/node/1173706 is fixed. + $date_format_types = \Drupal::entityTypeManager()->getStorage('date_format')->loadMultiple(); + foreach ($tokens as $name => $original) { + if (isset($date_format_types[$name]) && _token_module('date', $name) == 'token') { + $replacements[$original] = format_date($date, $name, '', NULL, $langcode); + } + } + } + + // Current date tokens. + // @todo Remove when http://drupal.org/node/943028 is fixed. + if ($type == 'current-date') { + $replacements += \Drupal::token()->generate('date', $tokens, array('date' => REQUEST_TIME), $options, $bubbleable_metadata); + } + + // Comment tokens. + if ($type == 'comment' && !empty($data['comment'])) { + /* @var \Drupal\comment\CommentInterface $comment */ + $comment = $data['comment']; + + // Chained token relationships. + if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) { + // Add fragment to url options. + $replacements += \Drupal::token()->generate('url', $url_tokens, array('url' => $comment->urlInfo('canonical', ['fragment' => "comment-{$comment->id()}"])), $options, $bubbleable_metadata); + } + } + + // Node tokens. + if ($type == 'node' && !empty($data['node'])) { + /* @var \Drupal\node\NodeInterface $node */ + $node = $data['node']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'log': + $replacements[$original] = (string) $node->revision_log->value; + break; + case 'content-type': + $type_name = \Drupal::entityTypeManager()->getStorage('node_type')->load($node->getType())->label(); + $replacements[$original] = $type_name; + break; + } + } + + // Chained token relationships. + if (($parent_tokens = \Drupal::token()->findWithPrefix($tokens, 'source')) && $source_node = $node->getUntranslated()) { + $replacements += \Drupal::token()->generate('node', $parent_tokens, array('node' => $source_node), $options, $bubbleable_metadata); + } + if (($node_type_tokens = \Drupal::token()->findWithPrefix($tokens, 'content-type')) && $node_type = node_type_load($node->bundle())) { + $replacements += \Drupal::token()->generate('content-type', $node_type_tokens, array('node_type' => $node_type), $options, $bubbleable_metadata); + } + if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) { + $replacements += \Drupal::token()->generate('url', $url_tokens, array('url' => $node->urlInfo()), $options, $bubbleable_metadata); + } + } + + // Content type tokens. + if ($type == 'content-type' && !empty($data['node_type'])) { + /* @var \Drupal\node\NodeTypeInterface $node_type */ + $node_type = $data['node_type']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'name': + $replacements[$original] = $node_type->label(); + break; + case 'machine-name': + $replacements[$original] = $node_type->id(); + break; + case 'description': + $replacements[$original] = $node_type->getDescription(); + break; + case 'node-count': + $count = \Drupal::entityQueryAggregate('node') + ->aggregate('nid', 'COUNT') + ->condition('type', $node_type->id()) + ->execute(); + $replacements[$original] = (int) $count; + break; + case 'edit-url': + $replacements[$original] = $node_type->url('edit-form', $url_options); + break; + } + } + } + + // Taxonomy term tokens. + if ($type == 'term' && !empty($data['term'])) { + /* @var \Drupal\taxonomy\TermInterface $term */ + $term = $data['term']; + + /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */ + $term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'edit-url': + $replacements[$original] = Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $term->id()], $url_options)->toString(); + break; + + case 'parents': + if ($parents = token_taxonomy_term_load_all_parents($term->id(), $langcode)) { + $replacements[$original] = token_render_array($parents, $options); + } + break; + + case 'root': + $parents = $term_storage->loadAllParents($term->id()); + $root_term = end($parents); + if ($root_term->id() != $term->id()) { + $replacements[$original] = $root_term->label(); + } + break; + } + } + + // Chained token relationships. + if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url'))) { + $replacements += \Drupal::token()->generate('url', $url_tokens, array('url' => $term->urlInfo()), $options, $bubbleable_metadata); + } + // [term:parents:*] chained tokens. + if ($parents_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) { + if ($parents = token_taxonomy_term_load_all_parents($term->id(), $langcode)) { + $replacements += \Drupal::token()->generate('array', $parents_tokens, array('array' => $parents), $options, $bubbleable_metadata); + } + } + if ($root_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) { + $parents = $term_storage->loadAllParents($term->id()); + $root_term = end($parents); + if ($root_term->tid != $term->id()) { + $replacements += \Drupal::token()->generate('term', $root_tokens, array('term' => $root_term), $options, $bubbleable_metadata); + } + } + } + + // Vocabulary tokens. + if ($type == 'vocabulary' && !empty($data['vocabulary'])) { + $vocabulary = $data['vocabulary']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'machine-name': + $replacements[$original] = $vocabulary->id(); + break; + case 'edit-url': + $replacements[$original] = Url::fromRoute('entity.taxonomy_vocabulary.edit_form', ['taxonomy_vocabulary' => $vocabulary->id()], $url_options)->toString(); + break; + } + } + } + + // File tokens. + if ($type == 'file' && !empty($data['file'])) { + $file = $data['file']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'basename': + $basename = pathinfo($file->uri->value, PATHINFO_BASENAME); + $replacements[$original] = $basename; + break; + case 'extension': + $extension = pathinfo($file->uri->value, PATHINFO_EXTENSION); + $replacements[$original] = $extension; + break; + case 'size-raw': + $replacements[$original] = (int) $file->filesize->value; + break; + } + } + } + + // User tokens. + if ($type == 'user' && !empty($data['user'])) { + /* @var \Drupal\user\UserInterface $account */ + $account = $data['user']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'picture': + if ($account instanceof UserInterface && $account->hasField('user_picture')) { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $output = [ + '#theme' => 'user_picture', + '#account' => $account, + ]; + $replacements[$original] = $renderer->renderPlain($output); + } + break; + + case 'roles': + $roles = $account->getRoles(); + $roles_names = array_combine($roles, $roles); + $replacements[$original] = token_render_array($roles_names, $options); + break; + } + } + + // Chained token relationships. + if ($account instanceof UserInterface && $account->hasField('user_picture') && ($picture_tokens = \Drupal::token()->findWithPrefix($tokens, 'picture'))) { + $replacements += \Drupal::token()->generate('file', $picture_tokens, array('file' => $account->user_picture->entity), $options, $bubbleable_metadata); + } + if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) { + $replacements += \Drupal::token()->generate('url', $url_tokens, array('url' => $account->urlInfo()), $options, $bubbleable_metadata); + } + if ($role_tokens = \Drupal::token()->findWithPrefix($tokens, 'roles')) { + $roles = $account->getRoles(); + $roles_names = array_combine($roles, $roles); + $replacements += \Drupal::token()->generate('array', $role_tokens, array('array' => $roles_names), $options, $bubbleable_metadata); + } + } + + // Current user tokens. + if ($type == 'current-user') { + foreach ($tokens as $name => $original) { + switch ($name) { + case 'ip-address': + $ip = \Drupal::request()->getClientIp(); + $replacements[$original] = $ip; + break; + } + } + } + + // Menu link tokens. + if ($type == 'menu-link' && !empty($data['menu-link'])) { + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = $data['menu-link']; + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + + if ($link instanceof MenuLinkContentInterface) { + $link = $menu_link_manager->createInstance($link->getPluginId()); + } + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'id': + $replacements[$original] = $link->getPluginId(); + break; + case 'title': + $replacements[$original] = token_menu_link_translated_title($link, $langcode); + break; + case 'url': + $replacements[$original] = $link->getUrlObject()->setAbsolute()->toString(); + break; + case 'parent': + + /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */ + if ($link->getParent() && $parent = $menu_link_manager->createInstance($link->getParent())) { + $replacements[$original] = token_menu_link_translated_title($parent, $langcode); + } + break; + case 'parents': + if ($parents = token_menu_link_load_all_parents($link->getPluginId(), $langcode)) { + $replacements[$original] = token_render_array($parents, $options); + } + break; + case 'root'; + if ($link->getParent() && $parent_ids = array_keys(token_menu_link_load_all_parents($link->getPluginId(), $langcode))) { + $root = $menu_link_manager->createInstance(array_shift($parent_ids)); + $replacements[$original] = token_menu_link_translated_title($root, $langcode); + } + break; + } + } + + // Chained token relationships. + /** @var \Drupal\Core\Menu\MenuLinkInterface $parent */ + if ($link->getParent() && ($parent_tokens = \Drupal::token()->findWithPrefix($tokens, 'parent')) && $parent = $menu_link_manager->createInstance($link->getParent())) { + $replacements += \Drupal::token()->generate('menu-link', $parent_tokens, array('menu-link' => $parent), $options, $bubbleable_metadata); + } + // [menu-link:parents:*] chained tokens. + if ($parents_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) { + if ($parents = token_menu_link_load_all_parents($link->getPluginId(), $langcode)) { + $replacements += \Drupal::token()->generate('array', $parents_tokens, array('array' => $parents), $options, $bubbleable_metadata); + } + } + if (($root_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) && $link->getParent() && $parent_ids = array_keys(token_menu_link_load_all_parents($link->getPluginId(), $langcode))) { + $root = $menu_link_manager->createInstance(array_shift($parent_ids)); + $replacements += \Drupal::token()->generate('menu-link', $root_tokens, array('menu-link' => $root), $options, $bubbleable_metadata); + } + if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) { + $replacements += \Drupal::token()->generate('url', $url_tokens, array('url' => $link->getUrlObject()), $options, $bubbleable_metadata); + } + + } + + // Current page tokens. + if ($type == 'current-page') { + foreach ($tokens as $name => $original) { + switch ($name) { + case 'title': + $request = \Drupal::request(); + $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT); + if ($route) { + $title = \Drupal::service('title_resolver')->getTitle($request, $route); + $replacements[$original] = token_render_array_value($title); + } + break; + case 'url': + $replacements[$original] = Url::fromRoute('', [], $url_options)->toString(); + break; + case 'page-number': + if ($page = \Drupal::request()->query->get('page')) { + // @see PagerDefault::execute() + $pager_page_array = explode(',', $page); + $page = $pager_page_array[0]; + } + $replacements[$original] = (int) $page + 1; + break; + } + } + + // @deprecated + // [current-page:arg] dynamic tokens. + if ($arg_tokens = \Drupal::token()->findWithPrefix($tokens, 'arg')) { + $path = ltrim(\Drupal::service('path.current')->getPath(), '/'); + // Make sure its a system path. + $path = \Drupal::service('path.alias_manager')->getPathByAlias($path); + foreach ($arg_tokens as $name => $original) { + $parts = explode('/', $path); + if (is_numeric($name) && isset($parts[$name])) { + $replacements[$original] = $parts[$name]; + } + } + } + + // [current-page:query] dynamic tokens. + if ($query_tokens = \Drupal::token()->findWithPrefix($tokens, 'query')) { + foreach ($query_tokens as $name => $original) { + if (\Drupal::request()->query->has($name)) { + $value = \Drupal::request()->query->get($name); + $replacements[$original] = $value; + } + } + } + + // Chained token relationships. + if ($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) { + $url = Url::fromRoute(''); + $replacements += \Drupal::token()->generate('url', $url_tokens, array('url' => $url), $options, $bubbleable_metadata); + } + } + + // URL tokens. + if ($type == 'url' && !empty($data['url'])) { + /** @var \Drupal\Core\Url $url */ + $url = $data['url']; + // To retrieve the correct path, modify a copy of the Url object. + $path_url = clone $url; + $path = '/' . $path_url->setAbsolute(FALSE)->setOption('fragment', NULL)->getInternalPath(); + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'path': + $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path; + $replacements[$original] = $value; + break; + case 'alias': + // @deprecated + $alias = \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode); + $replacements[$original] = $alias; + break; + case 'absolute': + $replacements[$original] = $url->setAbsolute()->toString(); + break; + case 'relative': + $replacements[$original] = $url->setAbsolute(FALSE)->toString(); + break; + case 'brief': + $replacements[$original] = preg_replace(array('!^https?://!', '!/$!'), '', $url->setAbsolute()->toString()); + break; + case 'unaliased': + $unaliased = clone $url; + $replacements[$original] = $unaliased->setAbsolute()->setOption('alias', TRUE)->toString(); + break; + case 'args': + $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path; + $replacements[$original] = token_render_array(explode('/', $value), $options); + break; + + } + } + + // [url:args:*] chained tokens. + if ($arg_tokens = \Drupal::token()->findWithPrefix($tokens, 'args')) { + $value = !($url->getOption('alias')) ? \Drupal::service('path.alias_manager')->getAliasByPath($path, $langcode) : $path; + $replacements += \Drupal::token()->generate('array', $arg_tokens, array('array' => explode('/', ltrim($value, '/'))), $options, $bubbleable_metadata); + } + + // [url:unaliased:*] chained tokens. + if ($unaliased_tokens = \Drupal::token()->findWithPrefix($tokens, 'unaliased')) { + $url->setOption('alias', TRUE); + $replacements += \Drupal::token()->generate('url', $unaliased_tokens, array('url' => $url), $options, $bubbleable_metadata); + } + } + + // Entity tokens. + if (!empty($data[$type]) && $entity_type = \Drupal::service('token.entity_mapper')->getEntityTypeForTokenType($type)) { + /* @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $data[$type]; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'url': + if (_token_module($type, 'url') == 'token' && $url = $entity->url()) { + $replacements[$original] = $url; + } + break; + + case 'original': + if (_token_module($type, 'original') == 'token' && !empty($entity->original)) { + $label = $entity->original->label(); + $replacements[$original] = $label; + } + break; + } + } + + // [entity:url:*] chained tokens. + if (($url_tokens = \Drupal::token()->findWithPrefix($tokens, 'url')) && _token_module($type, 'url') == 'token') { + $replacements += \Drupal::token()->generate('url', $url_tokens, array('url' => $entity->toUrl()), $options, $bubbleable_metadata); + } + + // [entity:original:*] chained tokens. + if (($original_tokens = \Drupal::token()->findWithPrefix($tokens, 'original')) && _token_module($type, 'original') == 'token' && !empty($entity->original)) { + $replacements += \Drupal::token()->generate($type, $original_tokens, array($type => $entity->original), $options, $bubbleable_metadata); + } + + // Pass through to an generic 'entity' token type generation. + $entity_data = array( + 'entity_type' => $entity_type, + 'entity' => $entity, + 'token_type' => $type, + ); + // @todo Investigate passing through more data like everything from entity_extract_ids(). + $replacements += \Drupal::token()->generate('entity', $tokens, $entity_data, $options, $bubbleable_metadata); + } + + // Array tokens. + if ($type == 'array' && !empty($data['array']) && is_array($data['array'])) { + $array = $data['array']; + + $sort = isset($options['array sort']) ? $options['array sort'] : TRUE; + $keys = token_element_children($array, $sort); + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'first': + $value = $array[$keys[0]]; + $value = is_array($value) ? $renderer->renderPlain($value) : (string) $value; + $replacements[$original] = $value; + break; + case 'last': + $value = $array[$keys[count($keys) - 1]]; + $value = is_array($value) ? $renderer->renderPlain($value) : (string) $value; + $replacements[$original] =$value; + break; + case 'count': + $replacements[$original] = count($keys); + break; + case 'keys': + $replacements[$original] = token_render_array($keys, $options); + break; + case 'reversed': + $reversed = array_reverse($array, TRUE); + $replacements[$original] = token_render_array($reversed, $options); + break; + case 'join': + $replacements[$original] = token_render_array($array, array('join' => '') + $options); + break; + } + } + + // [array:value:*] dynamic tokens. + if ($value_tokens = \Drupal::token()->findWithPrefix($tokens, 'value')) { + foreach ($value_tokens as $key => $original) { + if ($key[0] !== '#' && isset($array[$key])) { + $replacements[$original] = token_render_array_value($array[$key], $options); + } + } + } + + // [array:join:*] dynamic tokens. + if ($join_tokens = \Drupal::token()->findWithPrefix($tokens, 'join')) { + foreach ($join_tokens as $join => $original) { + $replacements[$original] = token_render_array($array, array('join' => $join) + $options); + } + } + + // [array:keys:*] chained tokens. + if ($key_tokens = \Drupal::token()->findWithPrefix($tokens, 'keys')) { + $replacements += \Drupal::token()->generate('array', $key_tokens, array('array' => $keys), $options, $bubbleable_metadata); + } + + // [array:reversed:*] chained tokens. + if ($reversed_tokens = \Drupal::token()->findWithPrefix($tokens, 'reversed')) { + $replacements += \Drupal::token()->generate('array', $reversed_tokens, array('array' => array_reverse($array, TRUE)), array('array sort' => FALSE) + $options, $bubbleable_metadata); + } + + // @todo Handle if the array values are not strings and could be chained. + } + + // Random tokens. + if ($type == 'random') { + foreach ($tokens as $name => $original) { + switch ($name) { + case 'number': + $replacements[$original] = mt_rand(); + break; + } + } + + // [custom:hash:*] dynamic token. + if ($hash_tokens = \Drupal::token()->findWithPrefix($tokens, 'hash')) { + $algos = hash_algos(); + foreach ($hash_tokens as $name => $original) { + if (in_array($name, $algos)) { + $replacements[$original] = hash($name, Crypt::randomBytes(55)); + } + } + } + } + + // If $type is a token type, $data[$type] is empty but $data[$entity_type] is + // not, re-run token replacements. + if (empty($data[$type]) && ($entity_type = \Drupal::service('token.entity_mapper')->getEntityTypeForTokenType($type)) && $entity_type != $type && !empty($data[$entity_type]) && empty($options['recursive'])) { + $data[$type] = $data[$entity_type]; + $options['recursive'] = TRUE; + $replacements += \Drupal::moduleHandler()->invokeAll('tokens', array($type, $tokens, $data, $options, $bubbleable_metadata)); + } + + // If the token type specifics a 'needs-data' value, and the value is not + // present in $data, then throw an error. + if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) { + // Only check when tests are running. + $type_info = \Drupal::token()->getTypeInfo($type); + if (!empty($type_info['needs-data']) && !isset($data[$type_info['needs-data']])) { + trigger_error(t('Attempting to perform token replacement for token type %type without required data', array('%type' => $type)), E_USER_WARNING); + } + } + + return $replacements; +} + +/** + * Implements hook_token_info() on behalf of book.module. + */ +function book_token_info() { + $info['types']['book'] = array( + 'name' => t('Book'), + 'description' => t('Tokens related to books.'), + 'needs-data' => 'book', + ); + + $info['tokens']['book']['title'] = array( + 'name' => t('Title'), + 'description' => t('Title of the book.'), + ); + $info['tokens']['book']['author'] = array( + 'name' => t('Author'), + 'description' => t('The author of the book.'), + 'type' => 'user', + ); + $info['tokens']['book']['root'] = array( + 'name' => t('Root'), + 'description' => t('Top level of the book.'), + 'type' => 'node', + ); + $info['tokens']['book']['parent'] = array( + 'name' => t('Parent'), + 'description' => t('Parent of the current page.'), + 'type' => 'node', + ); + $info['tokens']['book']['parents'] = array( + 'name' => t('Parents'), + 'description' => t("An array of all the node's parents, starting with the root."), + 'type' => 'array', + ); + + $info['tokens']['node']['book'] = array( + 'name' => t('Book'), + 'description' => t('The book page associated with the node.'), + 'type' => 'book', + ); + return $info; +} + +/** + * Implements hook_tokens() on behalf of book.module. + */ +function book_tokens($type, $tokens, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata) { + $replacements = array(); + + // Node tokens. + if ($type == 'node' && !empty($data['node'])) { + $book = $data['node']->book; + + if (!empty($book['bid'])) { + if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'book')) { + $child_node = Node::load($book['nid']); + $replacements += \Drupal::token()->generate('book', $book_tokens, array('book' => $child_node), $options, $bubbleable_metadata); + } + } + } + // Book tokens. + else if ($type == 'book' && !empty($data['book'])) { + $book = $data['book']->book; + + if (!empty($book['bid'])) { + $book_node = Node::load($book['bid']); + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'root': + case 'title': + $replacements[$original] = $book_node->getTitle(); + break; + case 'parent': + if (!empty($book['pid'])) { + $parent_node = Node::load($book['pid']); + $replacements[$original] = $parent_node->getTitle(); + } + break; + case 'parents': + if ($parents = token_book_load_all_parents($book)) { + $replacements[$original] = token_render_array($parents, $options); + } + break; + } + } + + if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'author')) { + $replacements += \Drupal::token()->generate('user', $book_tokens, array('user' => $book_node->getOwner()), $options, $bubbleable_metadata); + } + if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'root')) { + $replacements += \Drupal::token()->generate('node', $book_tokens, array('node' => $book_node), $options, $bubbleable_metadata); + } + if (!empty($book['pid']) && $book_tokens = \Drupal::token()->findWithPrefix($tokens, 'parent')) { + $parent_node = Node::load($book['pid']); + $replacements += \Drupal::token()->generate('node', $book_tokens, array('node' => $parent_node), $options, $bubbleable_metadata); + } + if ($book_tokens = \Drupal::token()->findWithPrefix($tokens, 'parents')) { + $parents = token_book_load_all_parents($book); + $replacements += \Drupal::token()->generate('array', $book_tokens, array('array' => $parents), $options, $bubbleable_metadata); + } + } + } + + return $replacements; +} + +/** + * Implements hook_token_info() on behalf of menu_ui.module. + */ +function menu_ui_token_info() { + // Menu tokens. + $info['types']['menu'] = array( + 'name' => t('Menus'), + 'description' => t('Tokens related to menus.'), + 'needs-data' => 'menu', + ); + $info['tokens']['menu']['name'] = array( + 'name' => t('Name'), + 'description' => t("The name of the menu."), + ); + $info['tokens']['menu']['machine-name'] = array( + 'name' => t('Machine-readable name'), + 'description' => t("The unique machine-readable name of the menu."), + ); + $info['tokens']['menu']['description'] = array( + 'name' => t('Description'), + 'description' => t('The optional description of the menu.'), + ); + $info['tokens']['menu']['menu-link-count'] = array( + 'name' => t('Menu link count'), + 'description' => t('The number of menu links belonging to the menu.'), + ); + $info['tokens']['menu']['edit-url'] = array( + 'name' => t('Edit URL'), + 'description' => t("The URL of the menu's edit page."), + ); + + $info['tokens']['menu-link']['menu'] = array( + 'name' => t('Menu'), + 'description' => t('The menu of the menu link.'), + 'type' => 'menu', + ); + $info['tokens']['menu-link']['edit-url'] = array( + 'name' => t('Edit URL'), + 'description' => t("The URL of the menu link's edit page."), + ); + $info['tokens']['node']['menu-link'] = array( + 'name' => t('Menu link'), + 'description' => t("The menu link for this node."), + 'type' => 'menu-link', + ); + + return $info; +} + +/** + * Implements hook_tokens() on behalf of menu_ui.module. + */ +function menu_ui_tokens($type, $tokens, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata) { + $replacements = array(); + + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + + $url_options = array('absolute' => TRUE); + if (isset($options['langcode'])) { + $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']); + $langcode = $options['langcode']; + } + else { + $langcode = NULL; + } + + // Node tokens. + if ($type == 'node' && !empty($data['node'])) { + /** @var \Drupal\node\NodeInterface $node */ + $node = $data['node']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'menu-link': + // On node-form save we populate a calculated field with a menu_link + // references. + // @see token_node_menu_link_submit() + if ($node->getFieldDefinition('menu_link') && $menu_link = $node->menu_link->entity) { + /** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */ + $replacements[$original] = $menu_link->getTitle(); + } + else { + $url = $node->toUrl(); + if ($links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters())) { + $link = _token_menu_link_best_match($node, $links); + $replacements[$original] = token_menu_link_translated_title($link, $langcode); + } + } + break; + } + + // Chained token relationships. + if ($menu_tokens = \Drupal::token()->findWithPrefix($tokens, 'menu-link')) { + if ($node->getFieldDefinition('menu_link') && $menu_link = $node->menu_link->entity) { + /** @var \Drupal\menu_link_content\MenuLinkContentInterface $menu_link */ + $replacements += \Drupal::token()->generate('menu-link', $menu_tokens, array('menu-link' => $menu_link), $options, $bubbleable_metadata); + } + else { + $url = $node->urlInfo(); + if ($links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters())) { + $link = _token_menu_link_best_match($node, $links); + $replacements += \Drupal::token()->generate('menu-link', $menu_tokens, array('menu-link' => $link), $options, $bubbleable_metadata); + } + } + } + } + } + + // Menu link tokens. + if ($type == 'menu-link' && !empty($data['menu-link'])) { + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = $data['menu-link']; + + if ($link instanceof MenuLinkContentInterface) { + $link = $menu_link_manager->createInstance($link->getPluginId()); + } + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'menu': + if ($menu = Menu::load($link->getMenuName())) { + $replacements[$original] = $menu->label(); + } + break; + + case 'edit-url': + $replacements[$original] = $link->getEditRoute()->setOptions($url_options)->toString(); + break; + } + } + + // Chained token relationships. + if (($menu_tokens = \Drupal::token()->findWithPrefix($tokens, 'menu')) && $menu = Menu::load($link->getMenuName())) { + $replacements += \Drupal::token()->generate('menu', $menu_tokens, array('menu' => $menu), $options, $bubbleable_metadata); + } + } + + // Menu tokens. + if ($type == 'menu' && !empty($data['menu'])) { + /** @var \Drupal\system\MenuInterface $menu */ + $menu = $data['menu']; + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'name': + $replacements[$original] = $menu->label(); + break; + + case 'machine-name': + $replacements[$original] = $menu->id(); + break; + + case 'description': + $replacements[$original] = $menu->getDescription(); + break; + + case 'menu-link-count': + $replacements[$original] = $menu_link_manager->countMenuLinks($menu->id()); + break; + + case 'edit-url': + $replacements[$original] = Url::fromRoute('entity.menu.edit_form', ['menu' => $menu->id()], $url_options)->toString(); + break; + } + } + } + + return $replacements; +} + +/** + * Returns a best matched link for a given node. + * + * If the url exists in multiple menus, default to the one set on the node + * itself. + * + * @param \Drupal\node\NodeInterface $node + * The node to look up the default menu settings from. + * @param array $links + * An array of instances keyed by plugin ID. + * + * @return \Drupal\Core\Menu\MenuLinkInterface + * A Link instance. + */ +function _token_menu_link_best_match(NodeInterface $node, array $links) { + // Get the menu ui defaults so we can determine what menu was + // selected for this node. This ensures that if the node was added + // to the menu via the node UI, we use that as a default. If it + // was not added via the node UI then grab the first in the + // retrieved array. + $defaults = menu_ui_get_menu_link_defaults($node); + if (isset($defaults['id']) && isset($links[$defaults['id']])) { + $link = $links[$defaults['id']]; + } + else { + $link = reset($links); + } + return $link; +} + +/** + * Implements hook_token_info_alter() on behalf of field.module. + * + * We use hook_token_info_alter() rather than hook_token_info() as other + * modules may already have defined some field tokens. + */ +function field_token_info_alter(&$info) { + $type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinitions(); + + // Attach field tokens to their respecitve entity tokens. + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) { + if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface')) { + continue; + } + + // Make sure a token type exists for this entity. + $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity_type_id); + if (empty($token_type)) { + continue; + } + + $fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type_id); + foreach ($fields as $field_name => $field) { + /** @var \Drupal\field\FieldStorageConfigInterface $field */ + // Ensure the token type exists. + if (!isset($info['types'][$token_type])) { + continue; + } + + // Ensure the token implements FieldStorageConfigInterface or is defined + // in token module. + $provider = ''; + if (isset($info['types'][$token_type]['module'])) { + $provider = $info['types'][$token_type]['module']; + } + if (!($field instanceof FieldStorageConfigInterface) && $provider != 'token') { + continue; + } + + // If a token already exists for this field, then don't add it. + if (isset($info['tokens'][$token_type][$field_name])) { + continue; + } + + if ($token_type == 'comment' && $field_name == 'comment_body') { + // Core provides the comment field as [comment:body]. + continue; + } + + // Do not define the token type if the field has no properties. + if (!$field->getPropertyDefinitions()) { + continue; + } + + // Generate a description for the token. + $labels = _token_field_label($entity_type_id, $field_name); + $label = array_shift($labels); + $params['@type'] = $type_info[$field->getType()]['label']; + if (!empty($labels)) { + $params['%labels'] = implode(', ', $labels); + $description = t('@type field. Also known as %labels.', $params); + } + else { + $description = t('@type field.', $params); + } + + $cardinality = $field->getCardinality(); + $cardinality = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $cardinality > 3) ? 3 : $cardinality; + $field_token_name = $token_type . '-' . $field_name; + $info['tokens'][$token_type][$field_name] = array( + 'name' => Html::escape($label), + 'description' => $description, + 'module' => 'token', + // For multivalue fields the field token is a list type. + 'type' => $cardinality > 1 ? "list<$field_token_name>" : $field_token_name, + ); + + // Field token type. + $info['types'][$field_token_name] = [ + 'name' => Html::escape($label), + 'description' => t('@label tokens.', ['@label' => Html::escape($label)]), + 'needs-data' => $field_token_name, + 'nested' => TRUE, + ]; + // Field list token type. + if ($cardinality > 1) { + $info['types']["list<$field_token_name>"] = array( + 'name' => t('List of @type values', array('@type' => Html::escape($label))), + 'description' => t('Tokens for lists of @type values.', array('@type' => Html::escape($label))), + 'needs-data' => "list<$field_token_name>", + 'nested' => TRUE, + ); + } + + // Show a different token for each field delta. + if ($cardinality > 1) { + for ($delta = 0; $delta < $cardinality; $delta++) { + $info['tokens']["list<$field_token_name>"][$delta] = [ + 'name' => t('@type type with delta @delta', ['@type' => Html::escape($label), '@delta' => $delta]), + 'module' => 'token', + 'type' => $field_token_name, + ]; + } + } + + // Property tokens. + foreach ($field->getPropertyDefinitions() as $property => $property_definition) { + if (is_subclass_of($property_definition->getClass(), 'Drupal\Core\TypedData\PrimitiveInterface')) { + $info['tokens'][$field_token_name][$property] = [ + 'name' => $property_definition->getLabel(), + 'description' => $property_definition->getDescription(), + 'module' => 'token', + ]; + } + elseif (($property_definition instanceof DataReferenceDefinitionInterface) && ($property_definition->getTargetDefinition() instanceof EntityDataDefinitionInterface)) { + $referenced_entity_type = $property_definition->getTargetDefinition()->getEntityTypeId(); + $referenced_token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($referenced_entity_type); + $info['tokens'][$field_token_name][$property] = [ + 'name' => $property_definition->getLabel(), + 'description' => $property_definition->getDescription(), + 'module' => 'token', + 'type' => $referenced_token_type, + ]; + } + } + // Provide image_with_image_style tokens for image fields. + if ($field->getType() == 'image') { + $image_styles = image_style_options(FALSE); + foreach ($image_styles as $style => $description) { + $info['tokens'][$field_token_name][$style] = [ + 'name' => $description, + 'description' => t('Represents the image in the given image style.'), + 'type' => 'image_with_image_style', + ]; + } + } + // Provide format token for datetime fields. + if ($field->getType() == 'datetime') { + $info['tokens'][$field_token_name]['date'] = $info['tokens'][$field_token_name]['value']; + $info['tokens'][$field_token_name]['date']['name'] .= ' ' . t('format'); + $info['tokens'][$field_token_name]['date']['type'] = 'date'; + } + if ($field->getType() == 'daterange') { + $info['tokens'][$field_token_name]['start_date'] = $info['tokens'][$field_token_name]['value']; + $info['tokens'][$field_token_name]['start_date']['name'] .= ' ' . t('format'); + $info['tokens'][$field_token_name]['start_date']['type'] = 'date'; + $info['tokens'][$field_token_name]['end_date'] = $info['tokens'][$field_token_name]['end_value']; + $info['tokens'][$field_token_name]['end_date']['name'] .= ' ' . t('format'); + $info['tokens'][$field_token_name]['end_date']['type'] = 'date'; + } + } + } +} + +/** + * Returns the label of a certain field. + * + * Therefore it looks up in all bundles to find the most used instance. + * + * Based on views_entity_field_label(). + * + * @todo Resync this method with views_entity_field_label(). + */ +function _token_field_label($entity_type, $field_name) { + $labels = []; + // Count the amount of instances per label per field. + foreach (array_keys(\Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type)) as $bundle) { + $bundle_instances = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type, $bundle); + if (isset($bundle_instances[$field_name])) { + $instance = $bundle_instances[$field_name]; + $label = (string) $instance->getLabel(); + $labels[$label] = isset($labels[$label]) ? ++$labels[$label] : 1; + } + } + + if (empty($labels)) { + return [$field_name]; + } + + // Sort the field labels by it most used label and return the labels. + arsort($labels); + return array_keys($labels); +} + +/** + * Implements hook_tokens() on behalf of field.module. + */ +function field_tokens($type, $tokens, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata) { + $replacements = array(); + $langcode = isset($options['langcode']) ? $options['langcode'] : NULL; + // Entity tokens. + if ($type == 'entity' && !empty($data['entity_type']) && !empty($data['entity']) && !empty($data['token_type'])) { + /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $data['entity']; + if (!($entity instanceof ContentEntityInterface)) { + return $replacements; + } + + if (!isset($options['langcode'])) { + // Set the active language in $options, so that it is passed along. + $langcode = $options['langcode'] = $entity->language()->getId(); + } + // Obtain the entity with the correct language. + $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode); + + $view_mode_name = $entity->getEntityTypeId() . '.' . $entity->bundle() . '.token'; + $view_display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->load($view_mode_name); + $token_view_display = (!empty($view_display) && $view_display->status()); + foreach ($tokens as $name => $original) { + // For the [entity:field_name] token. + if (strpos($name, ':') === FALSE) { + $field_name = $name; + $token_name = $name; + } + // For [entity:field_name:0], [entity:field_name:0:value] and + // [entity:field_name:value] tokens. + else { + list($field_name, $delta) = explode(':', $name, 2); + if (!is_numeric($delta)) { + unset($delta); + } + $token_name = $field_name; + } + // Ensure the entity has the requested field and that the token for it is + // defined by token.module. + if (!$entity->hasField($field_name) || _token_module($data['token_type'], $token_name) != 'token') { + continue; + } + + $display_options = 'token'; + // Do not continue if the field is empty. + if ($entity->get($field_name)->isEmpty()) { + continue; + } + // Handle [entity:field_name] and [entity:field_name:0] tokens. + if ($field_name === $name || isset($delta)) { + if (!$token_view_display) { + // We don't have the token view display and should fall back on + // default formatters. If the field has specified a specific formatter + // to be used by default with tokens, use that, otherwise use the + // default formatter. + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $field_type_definition = $field_type_manager->getDefinition($entity->getFieldDefinition($field_name)->getType()); + $display_options = [ + 'type' => !empty($field_type_definition['default_token_formatter']) ? $field_type_definition['default_token_formatter'] : $field_type_definition['default_formatter'], + 'label' => 'hidden', + ]; + } + + // Render only one delta. + if (isset($delta)) { + if ($field_delta = $entity->{$field_name}[$delta]) { + $field_output = $field_delta->view($display_options); + } + // If no such delta exists, let's not replace the token. + else { + continue; + } + } + // Render the whole field (with all deltas). + else { + $field_output = $entity->$field_name->view($display_options); + // If we are displaying all field items we need this #pre_render + // callback. + $field_output['#pre_render'][] = 'token_pre_render_field_token'; + } + $field_output['#token_options'] = $options; + $replacements[$original] = \Drupal::service('renderer')->renderPlain($field_output); + } + // Handle [entity:field_name:value] and [entity:field_name:0:value] + // tokens. + else if ($field_tokens = \Drupal::token()->findWithPrefix($tokens, $field_name)) { + $property_token_data = [ + 'field_property' => TRUE, + $data['entity_type'] . '-' . $field_name => $entity->$field_name, + 'field_name' => $data['entity_type'] . '-' . $field_name, + ]; + $replacements += \Drupal::token()->generate($field_name, $field_tokens, $property_token_data, $options, $bubbleable_metadata); + } + } + + // Remove the cloned object from memory. + unset($entity); + } + elseif (!empty($data['field_property'])) { + foreach ($tokens as $token => $original) { + $filtered_tokens = $tokens; + $delta = 0; + $parts = explode(':', $token); + if (is_numeric($parts[0])) { + if (count($parts) > 1) { + $delta = $parts[0]; + $property_name = $parts[1]; + // Pre-filter the tokens to select those with the correct delta. + $filtered_tokens = \Drupal::token()->findWithPrefix($tokens, $delta); + // Remove the delta to unify between having and not having one. + array_shift($parts); + } + else { + // Token is fieldname:delta, which is invalid. + continue; + } + } + else { + $property_name = $parts[0]; + } + + if (isset($data[$data['field_name']][$delta])) { + $field_item = $data[$data['field_name']][$delta]; + } + else { + // The field has no such delta, abort replacement. + continue; + } + + if (isset($field_item->$property_name) && ($field_item->$property_name instanceof FieldableEntityInterface)) { + // Entity reference field. + $entity = $field_item->$property_name; + // Obtain the referenced entity with the correct language. + $entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode); + + if (count($parts) > 1) { + $field_tokens = \Drupal::token()->findWithPrefix($filtered_tokens, $property_name); + $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity->getEntityTypeId(), TRUE); + $replacements += \Drupal::token()->generate($token_type, $field_tokens, [$token_type => $entity], $options, $bubbleable_metadata); + } + else { + $replacements[$original] = $entity->label(); + } + } + elseif (($field_item->getFieldDefinition()->getType() == 'image') && ($style = ImageStyle::load($property_name))) { + // Handle [node:field_name:image_style:property] tokens and multivalued + // [node:field_name:delta:image_style:property] tokens. If the token is + // of the form [node:field_name:image_style], provide the URL as a + // replacement. + $property_name = isset($parts[1]) ? $parts[1] : 'url'; + $entity = $field_item->entity; + $original_uri = $entity->getFileUri(); + + // Only generate the image derivative if needed. + if ($property_name === 'width' || $property_name === 'height') { + $dimensions = [ + 'width' => $field_item->width, + 'height' => $field_item->height, + ]; + $style->transformDimensions($dimensions, $original_uri); + $replacements[$original] = $dimensions[$property_name]; + } + elseif ($property_name === 'uri') { + $replacements[$original] = $style->buildUri($original_uri); + } + elseif ($property_name === 'url') { + $replacements[$original] = $style->buildUrl($original_uri); + } + else { + // Generate the image derivative, if it doesn't already exist. + $derivative_uri = $style->buildUri($original_uri); + $derivative_exists = TRUE; + if (!file_exists($derivative_uri)) { + $derivative_exists = $style->createDerivative($original_uri, $derivative_uri); + } + if ($derivative_exists) { + $image = \Drupal::service('image.factory')->get($derivative_uri); + // Provide the replacement. + switch ($property_name) { + case 'mimetype': + $replacements[$original] = $image->getMimeType(); + break; + case 'filesize' : + $replacements[$original] = $image->getFileSize(); + break; + } + } + } + } + elseif (in_array($field_item->getFieldDefinition()->getType(), ['datetime', 'daterange']) && in_array($property_name, ['date', 'start_date', 'end_date'])) { + $datetime = $field_item->$property_name->getTimestamp(); + if($property_name == $token) { + $replacements[$original] = $datetime; + } + else { + $field_tokens = \Drupal::token()->findWithPrefix($tokens, $property_name); + $replacements += \Drupal::token()->generate('date', $field_tokens, ['date' => $datetime], $options, $bubbleable_metadata); + } + } + else { + $replacements[$original] = $field_item->$property_name; + } + } + } + return $replacements; +} + +/** + * Pre-render callback for field output used with tokens. + */ +function token_pre_render_field_token($elements) { + // Remove the field theme hook, attachments, and JavaScript states. + unset($elements['#theme']); + unset($elements['#states']); + unset($elements['#attached']); + + // Prevent multi-value fields from appearing smooshed together by appending + // a join suffix to all but the last value. + $deltas = Element::getVisibleChildren($elements); + $count = count($deltas); + if ($count > 1) { + $join = isset($elements['#token_options']['join']) ? $elements['#token_options']['join'] : ", "; + foreach ($deltas as $index => $delta) { + // Do not add a suffix to the last item. + if ($index < ($count - 1)) { + $elements[$delta] += array('#suffix' => $join); + } + } + } + return $elements; +}