From f3baf763d342a5f82576890e2a8111a5aaf139dc Mon Sep 17 00:00:00 2001 From: Jeff Veit Date: Tue, 13 Nov 2018 22:21:54 +0000 Subject: [PATCH] Added Entity and Entity Reference Revisions which got dropped somewhere along the way, but are needed. --- composer.json | 4 +- composer.lock | 123 +++- vendor/composer/autoload_files.php | 4 +- vendor/composer/autoload_static.php | 4 +- vendor/composer/installed.json | 125 ++++ web/modules/contrib/entity/.travis.yml | 58 ++ web/modules/contrib/entity/LICENSE.txt | 339 +++++++++++ web/modules/contrib/entity/README.txt | 13 + web/modules/contrib/entity/composer.json | 10 + .../entity/config/schema/entity.schema.yml | 11 + web/modules/contrib/entity/entity.info.yml | 12 + .../contrib/entity/entity.links.action.yml | 2 + .../contrib/entity/entity.links.task.yml | 3 + web/modules/contrib/entity/entity.module | 120 ++++ .../contrib/entity/entity.permissions.yml | 2 + .../contrib/entity/entity.services.yml | 16 + web/modules/contrib/entity/entity.views.inc | 32 ++ .../EntityRevisionRouteAccessChecker.php | 175 ++++++ .../src/BundleEntityAccessControlHandler.php | 49 ++ .../entity/src/BundleFieldDefinition.php | 27 + .../src/BundlePlugin/BundlePluginHandler.php | 102 ++++ .../BundlePluginHandlerInterface.php | 37 ++ .../BundlePlugin/BundlePluginInstaller.php | 94 +++ .../BundlePluginInstallerInterface.php | 34 ++ .../BundlePlugin/BundlePluginInterface.php | 24 + .../BundlePluginUninstallValidator.php | 76 +++ .../Controller/RevisionControllerTrait.php | 192 +++++++ .../Controller/RevisionOverviewController.php | 160 ++++++ .../RevisionableEntityBundleInterface.php | 14 + .../entity/src/EntityAccessControlHandler.php | 60 ++ .../src/EntityAccessControlHandlerBase.php | 115 ++++ .../entity/src/EntityPermissionProvider.php | 95 +++ .../src/EntityPermissionProviderBase.php | 233 ++++++++ .../src/EntityPermissionProviderInterface.php | 23 + .../contrib/entity/src/EntityPermissions.php | 63 ++ .../contrib/entity/src/EntityViewBuilder.php | 17 + .../entity/src/Form/DeleteMultipleForm.php | 14 + .../entity/src/Form/RevisionRevertForm.php | 174 ++++++ .../Form/RevisionableContentEntityForm.php | 165 ++++++ .../entity/src/Menu/EntityAddLocalAction.php | 81 +++ .../EntityCollectionLocalActionProvider.php | 47 ++ .../EntityLocalActionProviderInterface.php | 23 + .../entity/src/Plugin/Action/DeleteAction.php | 20 + .../Action/Derivative/DeleteActionDeriver.php | 82 +++ .../Derivative/EntityActionsDeriver.php | 58 ++ .../Derivative/RevisionsOverviewDeriver.php | 68 +++ .../entity/src/QueryAccess/Condition.php | 113 ++++ .../entity/src/QueryAccess/ConditionGroup.php | 227 ++++++++ .../src/QueryAccess/EntityQueryAlter.php | 181 ++++++ .../src/QueryAccess/QueryAccessEvent.php | 92 +++ .../src/QueryAccess/QueryAccessHandler.php | 27 + .../QueryAccess/QueryAccessHandlerBase.php | 268 +++++++++ .../QueryAccessHandlerInterface.php | 41 ++ .../UncacheableQueryAccessHandler.php | 11 + .../src/QueryAccess/ViewsQueryAlter.php | 251 ++++++++ .../RevisionableContentEntityBase.php | 25 + .../src/Routing/AdminHtmlRouteProvider.php | 26 + .../src/Routing/DefaultHtmlRouteProvider.php | 26 + .../Routing/DeleteMultipleRouteProvider.php | 54 ++ .../src/Routing/RevisionRouteProvider.php | 127 ++++ .../UncacheableEntityAccessControlHandler.php | 54 ++ .../UncacheableEntityPermissionProvider.php | 145 +++++ ...odule_bundle_plugin_examples_test.info.yml | 13 + .../src/Plugin/BundlePluginTest/Second.php | 31 + .../entity_module_bundle_plugin_test.info.yml | 13 + ...ity_module_bundle_plugin_test.services.yml | 4 + .../src/Annotation/BundlePluginTest.php | 34 ++ .../src/BundlePluginTestManager.php | 35 ++ .../src/Entity/EntityTestBundlePlugin.php | 27 + .../BundlePluginTestInterface.php | 12 + .../src/Plugin/BundlePluginTest/First.php | 31 + ...ld.entity_test_enhanced.first.assigned.yml | 17 + ...d.entity_test_enhanced.second.assigned.yml | 17 + ....storage.entity_test_enhanced.assigned.yml | 20 + .../views.view.entity_test_enhanced.yml | 170 ++++++ ...ws.view.entity_test_enhanced_revisions.yml | 170 ++++++ ...s.view.entity_test_enhanced_with_owner.yml | 170 ++++++ ...ity_test_enhanced_with_owner_revisions.yml | 170 ++++++ .../entity_module_test.info.yml | 12 + .../entity_module_test.links.task.yml | 9 + .../entity_module_test.module | 15 + .../entity_module_test.permissions.yml | 3 + .../entity_module_test.services.yml | 5 + .../src/Entity/EnhancedEntity.php | 97 ++++ .../src/Entity/EnhancedEntityWithOwner.php | 145 +++++ .../EventSubscriber/QueryAccessSubscriber.php | 58 ++ .../Functional/CollectionRouteAccessTest.php | 71 +++ .../src/Functional/DeleteMultipleFormTest.php | 76 +++ .../Functional/Menu/EntityLocalActionTest.php | 44 ++ .../Functional/RevisionRouteAccessTest.php | 91 +++ .../tests/src/Kernel/BundlePluginTest.php | 146 +++++ .../tests/src/Kernel/DeleteActionTest.php | 78 +++ .../Kernel/QueryAccess/ConditionGroupTest.php | 106 ++++ .../QueryAccess/QueryAccessEventTest.php | 62 ++ .../QueryAccess/QueryAccessHandlerTest.php | 137 +++++ .../Kernel/QueryAccess/QueryAccessTest.php | 343 +++++++++++ .../UncacheableQueryAccessHandlerTest.php | 203 +++++++ .../UncacheableQueryAccessTest.php | 511 +++++++++++++++++ .../tests/src/Kernel/RevisionBasicUITest.php | 185 ++++++ .../RevisionOverviewIntegrationTest.php | 53 ++ .../BundleEntityAccessControlHandlerTest.php | 115 ++++ .../Unit/EntityAccessControlHandlerTest.php | 249 ++++++++ .../src/Unit/EntityPermissionProviderTest.php | 185 ++++++ .../src/Unit/QueryAccess/ConditionTest.php | 50 ++ ...acheableEntityAccessControlHandlerTest.php | 253 ++++++++ ...ncacheableEntityPermissionProviderTest.php | 190 ++++++ .../entity_reference_revisions/LICENSE.txt | 339 +++++++++++ .../entity_reference_revisions.schema.yml | 69 +++ ...ntity_reference_revisions.views.schema.yml | 20 + .../entity_reference_revisions.info.yml | 14 + .../entity_reference_revisions.module | 282 +++++++++ .../entity_reference_revisions.views.inc | 73 +++ .../src/EntityNeedsSaveInterface.php | 17 + .../src/EntityNeedsSaveTrait.php | 39 ++ .../EntityReferenceRevisionsFieldItemList.php | 151 +++++ ...ntityReferenceRevisionsServiceProvider.php | 35 ++ .../EntityReferenceRevisionItemNormalizer.php | 43 ++ .../DataType/EntityReferenceRevisions.php | 128 +++++ .../DataType/EntityRevisionsAdapter.php | 27 + .../MigrateEntityReferenceRevisions.php | 30 + ...ntityReferenceRevisionsEntityFormatter.php | 164 ++++++ .../EntityReferenceRevisionsFormatterBase.php | 30 + .../EntityReferenceRevisionsItem.php | 512 +++++++++++++++++ ...tyReferenceRevisionsAutocompleteWidget.php | 44 ++ ...tityReferenceRevisionsFieldDiffBuilder.php | 59 ++ .../destination/EntityReferenceRevisions.php | 219 +++++++ .../display/EntityReferenceRevisions.php | 176 ++++++ .../views/row/EntityReferenceRevisions.php | 57 ++ .../views/style/EntityReferenceRevisions.php | 103 ++++ .../EntityReferenceRevisionsAdminTest.php | 184 ++++++ ...tityReferenceRevisionsAutocompleteTest.php | 147 +++++ ...ferenceRevisionsCoreVersionUiTestTrait.php | 35 ++ .../EntityReferenceRevisionsDiffTest.php | 121 ++++ ...EntityReferenceRevisionsNormalizerTest.php | 106 ++++ .../EntityRevisionDataDefinition.php | 50 ++ ...ntity_composite_relationship_test.info.yml | 14 + ...omposite_relationship_test.permissions.yml | 2 + .../EntityTestCompositeRelationship.php | 61 ++ .../EntityReferenceRevisionsCompositeTest.php | 423 ++++++++++++++ ...evisionsCompositeTranslatableFieldTest.php | 351 ++++++++++++ ...renceRevisionsCompositeTranslationTest.php | 459 +++++++++++++++ .../EntityReferenceRevisionsFormatterTest.php | 106 ++++ .../EntityReferenceRevisionsSaveTest.php | 321 +++++++++++ .../EntityReferenceRevisionsDeriverTest.php | 46 ++ ...ntityReferenceRevisionsDestinationTest.php | 540 ++++++++++++++++++ 145 files changed, 14885 insertions(+), 6 deletions(-) create mode 100644 web/modules/contrib/entity/.travis.yml create mode 100644 web/modules/contrib/entity/LICENSE.txt create mode 100644 web/modules/contrib/entity/README.txt create mode 100644 web/modules/contrib/entity/composer.json create mode 100644 web/modules/contrib/entity/config/schema/entity.schema.yml create mode 100644 web/modules/contrib/entity/entity.info.yml create mode 100644 web/modules/contrib/entity/entity.links.action.yml create mode 100644 web/modules/contrib/entity/entity.links.task.yml create mode 100644 web/modules/contrib/entity/entity.module create mode 100644 web/modules/contrib/entity/entity.permissions.yml create mode 100644 web/modules/contrib/entity/entity.services.yml create mode 100644 web/modules/contrib/entity/entity.views.inc create mode 100644 web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php create mode 100644 web/modules/contrib/entity/src/BundleEntityAccessControlHandler.php create mode 100644 web/modules/contrib/entity/src/BundleFieldDefinition.php create mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php create mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php create mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php create mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php create mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php create mode 100644 web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php create mode 100644 web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php create mode 100644 web/modules/contrib/entity/src/Controller/RevisionOverviewController.php create mode 100644 web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php create mode 100644 web/modules/contrib/entity/src/EntityAccessControlHandler.php create mode 100644 web/modules/contrib/entity/src/EntityAccessControlHandlerBase.php create mode 100644 web/modules/contrib/entity/src/EntityPermissionProvider.php create mode 100644 web/modules/contrib/entity/src/EntityPermissionProviderBase.php create mode 100644 web/modules/contrib/entity/src/EntityPermissionProviderInterface.php create mode 100644 web/modules/contrib/entity/src/EntityPermissions.php create mode 100644 web/modules/contrib/entity/src/EntityViewBuilder.php create mode 100644 web/modules/contrib/entity/src/Form/DeleteMultipleForm.php create mode 100644 web/modules/contrib/entity/src/Form/RevisionRevertForm.php create mode 100644 web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php create mode 100644 web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php create mode 100644 web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php create mode 100644 web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php create mode 100644 web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php create mode 100644 web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php create mode 100644 web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php create mode 100644 web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/Condition.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/ConditionGroup.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/EntityQueryAlter.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/QueryAccessEvent.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/QueryAccessHandler.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerBase.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerInterface.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/UncacheableQueryAccessHandler.php create mode 100644 web/modules/contrib/entity/src/QueryAccess/ViewsQueryAlter.php create mode 100644 web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php create mode 100644 web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php create mode 100644 web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php create mode 100644 web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php create mode 100644 web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php create mode 100644 web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php create mode 100644 web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.first.assigned.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.second.assigned.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.storage.entity_test_enhanced.assigned.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_revisions.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner_revisions.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.module create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.services.yml create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityWithOwner.php create mode 100644 web/modules/contrib/entity/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php create mode 100644 web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php create mode 100644 web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php create mode 100644 web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php create mode 100644 web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/QueryAccess/ConditionGroupTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessEventTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessHandlerTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessHandlerTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessTest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php create mode 100644 web/modules/contrib/entity/tests/src/Kernel/RevisionOverviewIntegrationTest.php create mode 100644 web/modules/contrib/entity/tests/src/Unit/BundleEntityAccessControlHandlerTest.php create mode 100644 web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php create mode 100644 web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php create mode 100644 web/modules/contrib/entity/tests/src/Unit/QueryAccess/ConditionTest.php create mode 100644 web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php create mode 100644 web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/LICENSE.txt create mode 100644 web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml create mode 100644 web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml create mode 100644 web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml create mode 100644 web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module create mode 100644 web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc create mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml create mode 100644 web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml create mode 100644 web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php create mode 100644 web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php diff --git a/composer.json b/composer.json index 5e6145fec..c514b3110 100644 --- a/composer.json +++ b/composer.json @@ -89,7 +89,9 @@ "drupal/inline_entity_form": "^1.0@beta", "drupal/entity_embed": "^1.0@beta", "drupal/dropzonejs": "^2.0@alpha", - "drupal/security_review": "1.x-dev" + "drupal/security_review": "1.x-dev", + "drupal/entity_reference_revisions": "^1.6", + "drupal/entity": "^1.0-rc1" }, "require-dev": { "behat/mink": "~1.7", diff --git a/composer.lock b/composer.lock index ccd2b1cc1..38cdf935f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9bed23b474ece9af626936ebc6cb7d4b", + "content-hash": "2c9cad0804408626752bffa44fe2bec6", "packages": [ { "name": "alchemy/zippy", @@ -3365,6 +3365,69 @@ "irc": "irc://irc.freenode.org/drupal-media" } }, + { + "name": "drupal/entity", + "version": "1.0.0-rc1", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/entity", + "reference": "8.x-1.0-rc1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/entity-8.x-1.0-rc1.zip", + "reference": "8.x-1.0-rc1", + "shasum": "f2dcd7afd0d36b6f261b10a204057a5c801ad20b" + }, + "require": { + "drupal/core": "^8.6" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.0-rc1", + "datestamp": "1539272769", + "security-coverage": { + "status": "not-covered", + "message": "RC releases are not covered by Drupal security advisories." + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, + { + "name": "bojanz", + "homepage": "https://www.drupal.org/user/86106" + }, + { + "name": "dawehner", + "homepage": "https://www.drupal.org/user/99340" + }, + { + "name": "dixon_", + "homepage": "https://www.drupal.org/user/239911" + }, + { + "name": "fago", + "homepage": "https://www.drupal.org/user/16747" + } + ], + "description": "Provides expanded entity APIs, which will be moved to Drupal core one day.", + "homepage": "http://drupal.org/project/entity", + "support": { + "source": "http://cgit.drupalcode.org/entity" + } + }, { "name": "drupal/entity_browser", "version": "1.6.0", @@ -3514,6 +3577,64 @@ "irc": "irc://irc.freenode.org/drupal-media" } }, + { + "name": "drupal/entity_reference_revisions", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/entity_reference_revisions", + "reference": "8.x-1.6" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.6.zip", + "reference": "8.x-1.6", + "shasum": "82d515de04d3a75fb677ed82241a6aff3f54ab47" + }, + "require": { + "drupal/core": "~8.0" + }, + "require-dev": { + "drupal/diff": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.6", + "datestamp": "1539588781", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Frans", + "homepage": "https://www.drupal.org/user/514222" + }, + { + "name": "jeroen.b", + "homepage": "https://www.drupal.org/user/1853532" + }, + { + "name": "miro_dietiker", + "homepage": "https://www.drupal.org/user/227761" + } + ], + "description": "Adds a Entity Reference field type with revision support.", + "homepage": "https://www.drupal.org/project/entity_reference_revisions", + "support": { + "source": "http://cgit.drupalcode.org/entity_reference_revisions" + } + }, { "name": "drupal/entityqueue", "version": "1.0.0-alpha8", diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index 9efa99a86..981c16091 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -24,9 +24,9 @@ return array( '1ca3bc274755662169f9629d5412a1da' => $vendorDir . '/zendframework/zend-diactoros/src/functions/normalize_uploaded_files.php', '40360c0b9b437e69bcbb7f1349ce029e' => $vendorDir . '/zendframework/zend-diactoros/src/functions/parse_cookie_header.php', '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', + 'fbeead2280a8f3911a1fe6dd034f7d5e' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', + '96f8d8288528d52059397cad6ec61f17' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', '801c31d8ed748cfa537fa45402288c95' => $vendorDir . '/psy/psysh/src/functions.php', '952683d815ff0a7bf322b93c0be7e4e4' => $vendorDir . '/chi-teck/drupal-code-generator/src/bootstrap.php', '5a12a5271c58108e0aa33355e6ac54ea' => $vendorDir . '/drupal/console-core/src/functions.php', - 'fbeead2280a8f3911a1fe6dd034f7d5e' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', - '96f8d8288528d52059397cad6ec61f17' => $vendorDir . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index df87bb0e1..969c0c0c8 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -25,11 +25,11 @@ class ComposerStaticInit045d6a3105edf51cf91c16e965235549 '1ca3bc274755662169f9629d5412a1da' => __DIR__ . '/..' . '/zendframework/zend-diactoros/src/functions/normalize_uploaded_files.php', '40360c0b9b437e69bcbb7f1349ce029e' => __DIR__ . '/..' . '/zendframework/zend-diactoros/src/functions/parse_cookie_header.php', '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', + 'fbeead2280a8f3911a1fe6dd034f7d5e' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', + '96f8d8288528d52059397cad6ec61f17' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', '801c31d8ed748cfa537fa45402288c95' => __DIR__ . '/..' . '/psy/psysh/src/functions.php', '952683d815ff0a7bf322b93c0be7e4e4' => __DIR__ . '/..' . '/chi-teck/drupal-code-generator/src/bootstrap.php', '5a12a5271c58108e0aa33355e6ac54ea' => __DIR__ . '/..' . '/drupal/console-core/src/functions.php', - 'fbeead2280a8f3911a1fe6dd034f7d5e' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvancedInterface.php', - '96f8d8288528d52059397cad6ec61f17' => __DIR__ . '/..' . '/mkalkbrenner/php-htmldiff-advanced/src/HtmlDiffAdvanced.php', ); public static $prefixLengthsPsr4 = array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index d8035451f..4240b09b1 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -3750,6 +3750,71 @@ "irc": "irc://irc.freenode.org/drupal-media" } }, + { + "name": "drupal/entity", + "version": "1.0.0-rc1", + "version_normalized": "1.0.0.0-RC1", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/entity", + "reference": "8.x-1.0-rc1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/entity-8.x-1.0-rc1.zip", + "reference": "8.x-1.0-rc1", + "shasum": "f2dcd7afd0d36b6f261b10a204057a5c801ad20b" + }, + "require": { + "drupal/core": "^8.6" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.0-rc1", + "datestamp": "1539272769", + "security-coverage": { + "status": "not-covered", + "message": "RC releases are not covered by Drupal security advisories." + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, + { + "name": "bojanz", + "homepage": "https://www.drupal.org/user/86106" + }, + { + "name": "dawehner", + "homepage": "https://www.drupal.org/user/99340" + }, + { + "name": "dixon_", + "homepage": "https://www.drupal.org/user/239911" + }, + { + "name": "fago", + "homepage": "https://www.drupal.org/user/16747" + } + ], + "description": "Provides expanded entity APIs, which will be moved to Drupal core one day.", + "homepage": "http://drupal.org/project/entity", + "support": { + "source": "http://cgit.drupalcode.org/entity" + } + }, { "name": "drupal/entity_browser", "version": "1.6.0", @@ -3903,6 +3968,66 @@ "irc": "irc://irc.freenode.org/drupal-media" } }, + { + "name": "drupal/entity_reference_revisions", + "version": "1.6.0", + "version_normalized": "1.6.0.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/entity_reference_revisions", + "reference": "8.x-1.6" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/entity_reference_revisions-8.x-1.6.zip", + "reference": "8.x-1.6", + "shasum": "82d515de04d3a75fb677ed82241a6aff3f54ab47" + }, + "require": { + "drupal/core": "~8.0" + }, + "require-dev": { + "drupal/diff": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + }, + "drupal": { + "version": "8.x-1.6", + "datestamp": "1539588781", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Frans", + "homepage": "https://www.drupal.org/user/514222" + }, + { + "name": "jeroen.b", + "homepage": "https://www.drupal.org/user/1853532" + }, + { + "name": "miro_dietiker", + "homepage": "https://www.drupal.org/user/227761" + } + ], + "description": "Adds a Entity Reference field type with revision support.", + "homepage": "https://www.drupal.org/project/entity_reference_revisions", + "support": { + "source": "http://cgit.drupalcode.org/entity_reference_revisions" + } + }, { "name": "drupal/entityqueue", "version": "1.0.0-alpha8", diff --git a/web/modules/contrib/entity/.travis.yml b/web/modules/contrib/entity/.travis.yml new file mode 100644 index 000000000..e48d5d7d3 --- /dev/null +++ b/web/modules/contrib/entity/.travis.yml @@ -0,0 +1,58 @@ +language: php +sudo: false + +php: + - 5.5 + - 5.6 + - 7 + - hhvm + +matrix: + allow_failures: + # We cannot use hhvm-nightly since that does not work in Travis CI's old + # Ubuntu 12.04. + - php: hhvm + # Don't wait for the allowed failures to build. + fast_finish: true + +mysql: + database: entity + username: root + encoding: utf8 + +before_script: + # Remove Xdebug as we don't need it and it causes + # PHP Fatal error: Maximum function nesting level of '256' reached. + # We also don't care if that file exists or not on PHP 7. + - phpenv config-rm xdebug.ini || true + + # Remember the current entity test directory for later use in the Drupal + # installation. + - TESTDIR=$(pwd) + # Navigate out of module directory to prevent blown stack by recursive module + # lookup. + - cd .. + + # Create database. + - mysql -e 'create database entity' + # Export database variable for kernel tests. + - export SIMPLETEST_DB=mysql://root:@127.0.0.1/entity + # Download Drupal 8 core. + - travis_retry git clone --branch 8.5.x --depth 1 http://git.drupal.org/project/drupal.git + - cd drupal + - composer self-update + - composer install -n + + # Reference entity in build site. + - ln -s $TESTDIR modules/entity + + # Start a web server on port 8888, run in the background; wait for + # initialization. + - nohup php -S localhost:8888 > /dev/null 2>&1 & + + # Export web server URL for browser tests. + - export SIMPLETEST_BASE_URL=http://localhost:8888 + +script: + # Run the PHPUnit tests which also include the kernel tests. + - ./vendor/phpunit/phpunit/phpunit -c ./core/phpunit.xml.dist --verbose ./modules/entity diff --git a/web/modules/contrib/entity/LICENSE.txt b/web/modules/contrib/entity/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/web/modules/contrib/entity/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/entity/README.txt b/web/modules/contrib/entity/README.txt new file mode 100644 index 000000000..c43f96ccd --- /dev/null +++ b/web/modules/contrib/entity/README.txt @@ -0,0 +1,13 @@ +Entity API module +----------------- + +Provides improvements and extensions to the Drupal 8 Entity system. +Acts as a staging ground for Drupal core, with each core minor release (8.5, 8.6, 8.7) +receiving a portion of this module's functionality. + +Current functionality: +- Local action providers (core issue: #2976861) +- Permission providers (core issue: #2809177) +- Query access API (Change record: https://www.drupal.org/node/3002038, core issue: #777578) +- Bundle plugin API (plugin-based entity bundles, currently not proposed for core inclusion) +- A generic UI for revisions (WIP, see #2625122) diff --git a/web/modules/contrib/entity/composer.json b/web/modules/contrib/entity/composer.json new file mode 100644 index 000000000..ab1420aef --- /dev/null +++ b/web/modules/contrib/entity/composer.json @@ -0,0 +1,10 @@ +{ + "name": "drupal/entity", + "type": "drupal-module", + "description": "Provides expanded entity APIs, which will be moved to Drupal core one day.", + "homepage": "http://drupal.org/project/entity", + "license": "GPL-2.0+", + "require": { + "drupal/core": "^8.6" + } +} diff --git a/web/modules/contrib/entity/config/schema/entity.schema.yml b/web/modules/contrib/entity/config/schema/entity.schema.yml new file mode 100644 index 000000000..b3c192358 --- /dev/null +++ b/web/modules/contrib/entity/config/schema/entity.schema.yml @@ -0,0 +1,11 @@ +action.configuration.entity_delete_action:*: + type: action_configuration_default + label: 'Delete entity configuration' + +views.field.rendered_entity: + type: views_field + label: 'Rendered entity' + mapping: + view_mode: + type: string + label: 'View mode' diff --git a/web/modules/contrib/entity/entity.info.yml b/web/modules/contrib/entity/entity.info.yml new file mode 100644 index 000000000..473f3ed38 --- /dev/null +++ b/web/modules/contrib/entity/entity.info.yml @@ -0,0 +1,12 @@ +name: Entity +description: Provides expanded entity APIs, which will be moved to Drupal core one day. +type: module +# core: 8.x +dependencies: + - drupal:system (>=8.6.0) + +# Information added by Drupal.org packaging script on 2018-10-11 +version: '8.x-1.0-rc1' +core: '8.x' +project: 'entity' +datestamp: 1539272605 diff --git a/web/modules/contrib/entity/entity.links.action.yml b/web/modules/contrib/entity/entity.links.action.yml new file mode 100644 index 000000000..5e02edcd7 --- /dev/null +++ b/web/modules/contrib/entity/entity.links.action.yml @@ -0,0 +1,2 @@ +entity.entity_actions: + deriver: Drupal\entity\Plugin\Derivative\EntityActionsDeriver diff --git a/web/modules/contrib/entity/entity.links.task.yml b/web/modules/contrib/entity/entity.links.task.yml new file mode 100644 index 000000000..8116ae326 --- /dev/null +++ b/web/modules/contrib/entity/entity.links.task.yml @@ -0,0 +1,3 @@ +entity.revisions_overview: + deriver: 'Drupal\entity\Plugin\Derivative\RevisionsOverviewDeriver' + weight: 100 diff --git a/web/modules/contrib/entity/entity.module b/web/modules/contrib/entity/entity.module new file mode 100644 index 000000000..546f32278 --- /dev/null +++ b/web/modules/contrib/entity/entity.module @@ -0,0 +1,120 @@ +getDefinitions(); + $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { + return $entity_type->hasHandlerClass('bundle_plugin'); + }); + + return $entity_types; +} + +/** + * Implements hook_entity_type_build(). + */ +function entity_entity_type_build(array &$entity_types) { + foreach ($entity_types as $entity_type) { + if ($entity_type->get('bundle_plugin_type')) { + $entity_type->setHandlerClass('bundle_plugin', BundlePluginHandler::class); + } + } +} + +/** + * Implements hook_entity_bundle_info(). + */ +function entity_entity_bundle_info() { + $bundles = []; + foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { + /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ + $bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin'); + $bundles[$entity_type->id()] = $bundle_handler->getBundleInfo(); + } + return $bundles; +} + +/** + * Implements hook_entity_field_storage_info(). + */ +function entity_entity_field_storage_info(EntityTypeInterface $entity_type) { + if ($entity_type->hasHandlerClass('bundle_plugin')) { + /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ + $bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin'); + return $bundle_handler->getFieldStorageDefinitions(); + } +} + +/** + * Implements hook_entity_bundle_field_info(). + */ +function entity_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) { + if ($entity_type->hasHandlerClass('bundle_plugin')) { + /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ + $bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin'); + return $bundle_handler->getFieldDefinitions($bundle); + } +} + +/** + * Implements hook_modules_installed(). + */ +function entity_modules_installed($modules) { + foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { + \Drupal::service('entity.bundle_plugin_installer')->installBundles($entity_type, $modules); + } +} + +/** + * Implements hook_module_preuninstall(). + */ +function entity_module_preuninstall($module) { + foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { + \Drupal::service('entity.bundle_plugin_installer')->uninstallBundles($entity_type, [$module]); + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function entity_query_entity_query_alter(SelectInterface $query) { + $entity_type_id = $query->getMetaData('entity_type'); + if ($query->hasTag($entity_type_id . '_access')) { + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_type = $entity_type_manager->getDefinition($entity_type_id); + + \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityQueryAlter::class) + ->alter($query, $entity_type); + } +} + +/** + * Implements hook_views_query_alter(). + */ +function entity_views_query_alter(ViewExecutable $view, QueryPluginBase $query) { + if ($query instanceof Sql) { + \Drupal::service('class_resolver') + ->getInstanceFromDefinition(ViewsQueryAlter::class) + ->alter($query, $view); + } +} diff --git a/web/modules/contrib/entity/entity.permissions.yml b/web/modules/contrib/entity/entity.permissions.yml new file mode 100644 index 000000000..1676e880b --- /dev/null +++ b/web/modules/contrib/entity/entity.permissions.yml @@ -0,0 +1,2 @@ +permission_callbacks: + - \Drupal\entity\EntityPermissions::buildPermissions diff --git a/web/modules/contrib/entity/entity.services.yml b/web/modules/contrib/entity/entity.services.yml new file mode 100644 index 000000000..3e690d122 --- /dev/null +++ b/web/modules/contrib/entity/entity.services.yml @@ -0,0 +1,16 @@ +services: + access_checker.entity_revision: + class: \Drupal\entity\Access\EntityRevisionRouteAccessChecker + arguments: ['@entity_type.manager', '@current_route_match'] + tags: + - { name: access_check, applies_to: _entity_access_revision } + + entity.bundle_plugin_installer: + class: Drupal\entity\BundlePlugin\BundlePluginInstaller + arguments: ['@entity_type.manager', '@entity_bundle.listener', '@field_storage_definition.listener', '@field_definition.listener'] + + entity.bundle_plugin.uninstall_validator: + class: \Drupal\entity\BundlePlugin\BundlePluginUninstallValidator + tags: + - { name: module_install.uninstall_validator } + arguments: ['@entity_type.manager', '@string_translation'] diff --git a/web/modules/contrib/entity/entity.views.inc b/web/modules/contrib/entity/entity.views.inc new file mode 100644 index 000000000..3074175c6 --- /dev/null +++ b/web/modules/contrib/entity/entity.views.inc @@ -0,0 +1,32 @@ +getDefinitions(); + $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { + return $entity_type->entityClassImplements(ContentEntityInterface::class); + }); + + $data = []; + foreach ($entity_types as $entity_type) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + $base_table = $entity_type->getBaseTable() ?: $entity_type->id(); + + if ($entity_type->hasViewBuilderClass()) { + $data[$base_table]['rendered_entity'] = [ + 'field' => [ + 'title' => t('Rendered entity'), + 'help' => t('Renders an entity in a view mode.'), + 'id' => 'rendered_entity', + ], + ]; + } + } + + return $data; +} diff --git a/web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php b/web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php new file mode 100644 index 000000000..799af140b --- /dev/null +++ b/web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php @@ -0,0 +1,175 @@ +entityTypeManager = $entity_type_manager; + $this->routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, AccountInterface $account, RouteMatchInterface $route_match = NULL) { + if (empty($route_match)) { + $route_match = $this->routeMatch; + } + + $operation = $route->getRequirement('_entity_access_revision'); + list($entity_type_id, $operation) = explode('.', $operation, 2); + + if ($operation === 'list') { + $_entity = $route_match->getParameter($entity_type_id); + return AccessResult::allowedIf($this->checkAccess($_entity, $account, $operation))->cachePerPermissions(); + } + else { + $_entity_revision = $route_match->getParameter($entity_type_id . '_revision'); + return AccessResult::allowedIf($_entity_revision && $this->checkAccess($_entity_revision, $account, $operation))->cachePerPermissions(); + } + } + + /** + * Performs access checks. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity for which to check access. + * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to check access. + * @param string $operation + * The entity operation. Usually one of 'view', 'view label', 'update' or + * 'delete'. + * + * @return bool + * The access result. + */ + protected function checkAccess(ContentEntityInterface $entity, AccountInterface $account, $operation = 'view') { + $entity_type = $entity->getEntityType(); + $entity_type_id = $entity->getEntityTypeId(); + $entity_access = $this->entityTypeManager->getAccessControlHandler($entity_type_id); + + /** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); + + $map = [ + 'view' => "view all $entity_type_id revisions", + 'list' => "view all $entity_type_id revisions", + 'update' => "revert all $entity_type_id revisions", + 'delete' => "delete all $entity_type_id revisions", + ]; + $bundle = $entity->bundle(); + $type_map = [ + 'view' => "view $entity_type_id $bundle revisions", + 'list' => "view $entity_type_id $bundle revisions", + 'update' => "revert $entity_type_id $bundle revisions", + 'delete' => "delete $entity_type_id $bundle revisions", + ]; + + if (!$entity || !isset($map[$operation]) || !isset($type_map[$operation])) { + // If there was no node to check against, or the $op was not one of the + // supported ones, we return access denied. + return FALSE; + } + + // Statically cache access by revision ID, language code, user account ID, + // and operation. + $langcode = $entity->language()->getId(); + $cid = $entity->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $operation; + + if (!isset($this->accessCache[$cid])) { + $admin_permission = $entity_type->getAdminPermission(); + + // Perform basic permission checks first. + if (!$account->hasPermission($map[$operation]) && !$account->hasPermission($type_map[$operation]) && ($admin_permission && !$account->hasPermission($admin_permission))) { + $this->accessCache[$cid] = FALSE; + return FALSE; + } + + if (($admin_permission = $entity_type->getAdminPermission()) && $account->hasPermission($admin_permission)) { + $this->accessCache[$cid] = TRUE; + } + else { + // Entity access handlers are generally not aware of the "list" operation. + $operation = $operation == 'list' ? 'view' : $operation; + // First check the access to the default revision and finally, if the + // node passed in is not the default revision then access to that, too. + $this->accessCache[$cid] = $entity_access->access($entity_storage->load($entity->id()), $operation, $account) && ($entity->isDefaultRevision() || $entity_access->access($entity, $operation, $account)); + } + } + + return $this->accessCache[$cid]; + } + + /** + * Counts the number of revisions in the default language. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage + * The entity storage. + * + * @return int + * The number of revisions in the default language. + */ + protected function countDefaultLanguageRevisions(ContentEntityInterface $entity, EntityStorageInterface $entity_storage) { + $entity_type = $entity->getEntityType(); + $count = $entity_storage->getQuery() + ->allRevisions() + ->condition($entity_type->getKey('id'), $entity->id()) + ->condition($entity_type->getKey('default_langcode'), 1) + ->count() + ->execute(); + return $count; + } + + /** + * Resets the access cache. + * + * @return $this + */ + public function resetAccessCache() { + $this->accessCache = []; + return $this; + } + +} diff --git a/web/modules/contrib/entity/src/BundleEntityAccessControlHandler.php b/web/modules/contrib/entity/src/BundleEntityAccessControlHandler.php new file mode 100644 index 000000000..be9bba591 --- /dev/null +++ b/web/modules/contrib/entity/src/BundleEntityAccessControlHandler.php @@ -0,0 +1,49 @@ +id(); + $entity_type_id = $this->entityType->getBundleOf(); + $permissions = [ + "administer $entity_type_id", + // View permissions provided by EntityPermissionProvider. + "view $entity_type_id", + "view $bundle $entity_type_id", + // View permissions provided by UncacheableEntityPermissionProvider. + "view own $entity_type_id", + "view any $entity_type_id", + "view own $bundle $entity_type_id", + "view any $bundle $entity_type_id", + ]; + + return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR'); + } + else { + return parent::checkAccess($entity, $operation, $account); + } + } + +} diff --git a/web/modules/contrib/entity/src/BundleFieldDefinition.php b/web/modules/contrib/entity/src/BundleFieldDefinition.php new file mode 100644 index 000000000..9da74c87b --- /dev/null +++ b/web/modules/contrib/entity/src/BundleFieldDefinition.php @@ -0,0 +1,27 @@ +entityType = $entity_type; + $this->pluginManager = $plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('plugin.manager.' . $entity_type->get('bundle_plugin_type')) + ); + } + + /** + * {@inheritdoc} + */ + public function getBundleInfo() { + $bundles = []; + foreach ($this->pluginManager->getDefinitions() as $plugin_id => $definition) { + $bundles[$plugin_id] = [ + 'label' => $definition['label'], + 'description' => isset($definition['description']) ? $definition['description'] : '', + 'translatable' => $this->entityType->isTranslatable(), + 'provider' => $definition['provider'], + ]; + } + return $bundles; + } + + /** + * {@inheritdoc} + */ + public function getFieldStorageDefinitions() { + $definitions = []; + foreach (array_keys($this->pluginManager->getDefinitions()) as $plugin_id) { + /** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */ + $plugin = $this->pluginManager->createInstance($plugin_id); + $definitions += $plugin->buildFieldDefinitions(); + } + // Ensure the presence of required keys which aren't set by the plugin. + foreach ($definitions as $field_name => $definition) { + $definition->setName($field_name); + $definition->setTargetEntityTypeId($this->entityType->id()); + $definitions[$field_name] = $definition; + } + + return $definitions; + } + + /** + * {@inheritdoc} + */ + public function getFieldDefinitions($bundle) { + /** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */ + $plugin = $this->pluginManager->createInstance($bundle); + $definitions = $plugin->buildFieldDefinitions(); + // Ensure the presence of required keys which aren't set by the plugin. + foreach ($definitions as $field_name => $definition) { + $definition->setName($field_name); + $definition->setTargetEntityTypeId($this->entityType->id()); + $definition->setTargetBundle($bundle); + $definitions[$field_name] = $definition; + } + + return $definitions; + } + +} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php new file mode 100644 index 000000000..5702fb58c --- /dev/null +++ b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php @@ -0,0 +1,37 @@ +entityTypeManager = $entity_type_manager; + $this->entityBundleListener = $entity_bundle_listener; + $this->fieldStorageDefinitionListener = $field_storage_definition_listener; + $this->fieldDefinitionListener = $field_definition_listener; + } + + /** + * {@inheritdoc} + */ + public function installBundles(EntityTypeInterface $entity_type, array $modules) { + $bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin'); + $bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) { + return in_array($bundle_info['provider'], $modules, TRUE); + }); + foreach (array_keys($bundles) as $bundle) { + $this->entityBundleListener->onBundleCreate($bundle, $entity_type->id()); + foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) { + $this->fieldStorageDefinitionListener->onFieldStorageDefinitionCreate($definition); + $this->fieldDefinitionListener->onFieldDefinitionCreate($definition); + } + } + } + + /** + * {@inheritdoc} + */ + public function uninstallBundles(EntityTypeInterface $entity_type, array $modules) { + $bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin'); + $bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) { + return in_array($bundle_info['provider'], $modules, TRUE); + }); + foreach (array_keys($bundles) as $bundle) { + $this->entityBundleListener->onBundleDelete($bundle, $entity_type->id()); + foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) { + $this->fieldDefinitionListener->onFieldDefinitionDelete($definition); + $this->fieldStorageDefinitionListener->onFieldStorageDefinitionDelete($definition); + } + } + } + +} diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php new file mode 100644 index 000000000..afeacee48 --- /dev/null +++ b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php @@ -0,0 +1,34 @@ +entityTypeManager = $entity_type_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function validate($module) { + $reasons = []; + + foreach (entity_get_bundle_plugin_entity_types() as $entity_type) { + /** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */ + $bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin'); + $bundles = $bundle_handler->getBundleInfo(); + + // We find all bundles which have to be removed due to the uninstallation. + $bundles_filtered_by_module = array_filter($bundles, function ($bundle_info) use ($module) { + return $module === $bundle_info['provider']; + }); + + if (!empty($bundles_filtered_by_module)) { + $bundle_keys_with_content = array_filter(array_keys($bundles_filtered_by_module), function ($bundle) use ($entity_type) { + $result = $this->entityTypeManager->getStorage($entity_type->id())->getQuery() + ->condition($entity_type->getKey('bundle'), $bundle) + ->range(0, 1) + ->execute(); + return !empty($result); + }); + + $bundles_with_content = array_intersect_key($bundles_filtered_by_module, array_flip($bundle_keys_with_content)); + + foreach ($bundles_with_content as $bundle) { + $reasons[] = $this->t('There is data for the bundle @bundle on the entity type @entity_type. Please remove all content before uninstalling the module.', [ + '@bundle' => $bundle['label'], + '@entity_type' => $entity_type->getLabel(), + ]); + } + } + } + + return $reasons; + } + +} diff --git a/web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php b/web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php new file mode 100644 index 000000000..6be025a6e --- /dev/null +++ b/web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php @@ -0,0 +1,192 @@ +getEntityType(); + $result = $this->entityTypeManager()->getStorage($entity_type->id())->getQuery() + ->allRevisions() + ->condition($entity_type->getKey('id'), $entity->id()) + ->sort($entity_type->getKey('revision'), 'DESC') + ->execute(); + return array_keys($result); + } + + /** + * Generates an overview table of older revisions of an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * An entity object. + * + * @return array + * A render array. + */ + protected function revisionOverview(ContentEntityInterface $entity) { + $langcode = $this->languageManager() + ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) + ->getId(); + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId()); + $revision_ids = $this->revisionIds($entity); + $entity_revisions = $entity_storage->loadMultipleRevisions($revision_ids); + + $header = [$this->t('Revision'), $this->t('Operations')]; + $rows = []; + foreach ($entity_revisions as $revision) { + $row = []; + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) { + $row[] = $this->getRevisionDescription($revision, $revision->isDefaultRevision()); + + if ($revision->isDefaultRevision()) { + $row[] = [ + 'data' => [ + '#prefix' => '', + '#markup' => $this->t('Current revision'), + '#suffix' => '', + ], + ]; + foreach ($row as &$current) { + $current['class'] = ['revision-current']; + } + } + else { + $links = $this->getOperationLinks($revision); + $row[] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + } + + $rows[] = $row; + } + + $build[$entity->getEntityTypeId() . '_revisions_table'] = [ + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + ]; + + // We have no clue about caching yet. + $build['#cache']['max-age'] = 0; + + return $build; + } + + /** + * Get the links of the operations for an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity_revision + * The entity to build the revision links for. + * + * @return array + * The operation links. + */ + protected function getOperationLinks(EntityInterface $entity_revision) { + $links = []; + if ($this->hasRevertRevisionAccess($entity_revision)) { + $links['revert'] = $this->buildRevertRevisionLink($entity_revision); + } + + if ($this->hasDeleteRevisionAccess($entity_revision)) { + $links['delete'] = $this->buildDeleteRevisionLink($entity_revision); + } + + return array_filter($links); + } + +} diff --git a/web/modules/contrib/entity/src/Controller/RevisionOverviewController.php b/web/modules/contrib/entity/src/Controller/RevisionOverviewController.php new file mode 100644 index 000000000..495b6a040 --- /dev/null +++ b/web/modules/contrib/entity/src/Controller/RevisionOverviewController.php @@ -0,0 +1,160 @@ +dateFormatter = $date_formatter; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('date.formatter'), $container->get('renderer')); + } + + /** + * {@inheritdoc} + */ + protected function hasDeleteRevisionAccess(EntityInterface $entity) { + return $this->currentUser()->hasPermission("delete all {$entity->id()} revisions"); + } + + /** + * {@inheritdoc} + */ + protected function buildRevertRevisionLink(EntityInterface $entity_revision) { + if ($entity_revision->hasLinkTemplate('revision-revert-form')) { + return [ + 'title' => t('Revert'), + 'url' => $entity_revision->toUrl('revision-revert-form'), + ]; + } + } + + /** + * {@inheritdoc} + */ + protected function buildDeleteRevisionLink(EntityInterface $entity_revision) { + if ($entity_revision->hasLinkTemplate('revision-delete-form')) { + return [ + 'title' => t('Delete'), + 'url' => $entity_revision->toUrl('revision-delete-form'), + ]; + } + } + + /** + * Generates an overview table of older revisions of an entity. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The route match. + * + * @return array + * A render array. + */ + public function revisionOverviewController(RouteMatchInterface $route_match) { + return $this->revisionOverview($route_match->getParameter($route_match->getRouteObject()->getOption('entity_type_id'))); + } + + /** + * {@inheritdoc} + */ + protected function getRevisionDescription(ContentEntityInterface $revision, $is_default = FALSE) { + /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\user\EntityOwnerInterface|\Drupal\Core\Entity\RevisionLogInterface $revision */ + if ($revision instanceof RevisionLogInterface) { + // Use revision link to link to revisions that are not active. + $date = $this->dateFormatter->format($revision->getRevisionCreationTime(), 'short'); + $link = $revision->toLink($date, 'revision'); + + // @todo: Simplify this when https://www.drupal.org/node/2334319 lands. + $username = [ + '#theme' => 'username', + '#account' => $revision->getRevisionUser(), + ]; + $username = $this->renderer->render($username); + } + else { + $link = $revision->toLink($revision->label(), 'revision'); + $username = ''; + + } + + $markup = ''; + if ($revision instanceof RevisionLogInterface) { + $markup = $revision->getRevisionLogMessage(); + } + + if ($username) { + $template = '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}

{{ message }}

{% endif %}'; + } + else { + $template = '{% trans %} {{ date }} {% endtrans %}{% if message %}

{{ message }}

{% endif %}'; + } + + $column = [ + 'data' => [ + '#type' => 'inline_template', + '#template' => $template, + '#context' => [ + 'date' => $link->toString(), + 'username' => $username, + 'message' => ['#markup' => $markup, '#allowed_tags' => Xss::getHtmlTagList()], + ], + ], + ]; + return $column; + } + + /** + * {@inheritdoc} + */ + protected function hasRevertRevisionAccess(EntityInterface $entity) { + return AccessResult::allowedIfHasPermission($this->currentUser(), "revert all {$entity->getEntityTypeId()} revisions")->orIf( + AccessResult::allowedIfHasPermission($this->currentUser(), "revert {$entity->bundle()} {$entity->getEntityTypeId()} revisions") + ); + } + +} diff --git a/web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php b/web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php new file mode 100644 index 000000000..18d392a06 --- /dev/null +++ b/web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php @@ -0,0 +1,14 @@ +hasHandlerClass('permission_provider') || !is_a($entity_type->getHandlerClass('permission_provider'), EntityPermissionProvider::class, TRUE)) { + throw new \Exception('\Drupal\entity\EntityAccessControlHandler requires the \Drupal\entity\EntityPermissionProvider permission provider.'); + } + } + + /** + * {@inheritdoc} + */ + protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\user\EntityOwnerInterface $entity */ + if ($operation === 'view') { + if ($entity instanceof EntityPublishedInterface && !$entity->isPublished()) { + if ($account->id() != $entity->getOwnerId()) { + // There's no permission for viewing other user's unpublished entity. + return AccessResult::neutral()->cachePerUser(); + } + + $permissions = [ + "view own unpublished {$entity->getEntityTypeId()}", + ]; + $result = AccessResult::allowedIfHasPermissions($account, $permissions)->cachePerUser(); + } + else { + $result = AccessResult::allowedIfHasPermissions($account, [ + "view {$entity->getEntityTypeId()}", + "view {$entity->bundle()} {$entity->getEntityTypeId()}", + ], 'OR'); + } + } + else { + $result = parent::checkEntityOwnerPermissions($entity, $operation, $account); + } + + return $result; + } + +} diff --git a/web/modules/contrib/entity/src/EntityAccessControlHandlerBase.php b/web/modules/contrib/entity/src/EntityAccessControlHandlerBase.php new file mode 100644 index 000000000..c0a63c7cb --- /dev/null +++ b/web/modules/contrib/entity/src/EntityAccessControlHandlerBase.php @@ -0,0 +1,115 @@ +prepareUser($account); + /** @var \Drupal\Core\Access\AccessResult $result */ + $result = parent::checkAccess($entity, $operation, $account); + + if ($result->isNeutral()) { + if ($entity instanceof EntityOwnerInterface) { + $result = $this->checkEntityOwnerPermissions($entity, $operation, $account); + } + else { + $result = $this->checkEntityPermissions($entity, $operation, $account); + } + } + + // Ensure that access is evaluated again when the entity changes. + return $result->addCacheableDependency($entity); + } + + /** + * Checks the entity operation and bundle permissions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to check access. + * @param string $operation + * The entity operation. Usually one of 'view', 'view label', 'update' or + * 'delete'. + * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to check access. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + protected function checkEntityPermissions(EntityInterface $entity, $operation, AccountInterface $account) { + $permissions = [ + "$operation {$entity->getEntityTypeId()}", + "$operation {$entity->bundle()} {$entity->getEntityTypeId()}", + ]; + + return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR'); + } + + /** + * Checks the entity operation and bundle permissions, with owners. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to check access. + * @param string $operation + * The entity operation. Usually one of 'view', 'view label', 'update' or + * 'delete'. + * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to check access. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\user\EntityOwnerInterface $entity */ + if ($account->id() == $entity->getOwnerId()) { + $permissions = [ + "$operation own {$entity->getEntityTypeId()}", + "$operation any {$entity->getEntityTypeId()}", + "$operation own {$entity->bundle()} {$entity->getEntityTypeId()}", + "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}", + ]; + } + else { + $permissions = [ + "$operation any {$entity->getEntityTypeId()}", + "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}", + ]; + } + $result = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR')->cachePerUser(); + + return $result; + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + $result = parent::checkCreateAccess($account, $context, $entity_bundle); + if ($result->isNeutral()) { + $permissions = [ + 'administer ' . $this->entityTypeId, + 'create ' . $this->entityTypeId, + ]; + if ($entity_bundle) { + $permissions[] = 'create ' . $entity_bundle . ' ' . $this->entityTypeId; + } + + $result = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR'); + } + + return $result; + } + +} diff --git a/web/modules/contrib/entity/src/EntityPermissionProvider.php b/web/modules/contrib/entity/src/EntityPermissionProvider.php new file mode 100644 index 000000000..70e791e47 --- /dev/null +++ b/web/modules/contrib/entity/src/EntityPermissionProvider.php @@ -0,0 +1,95 @@ +id(); + $plural_label = $entity_type->getPluralLabel(); + + $permissions["view {$entity_type_id}"] = [ + 'title' => $this->t('View @type', [ + '@type' => $plural_label, + ]), + ]; + + return $permissions; + } + + /** + * Builds permissions for the bundle granularity. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return array + * The permissions. + */ + protected function buildBundlePermissions(EntityTypeInterface $entity_type) { + $permissions = parent::buildBundlePermissions($entity_type); + $entity_type_id = $entity_type->id(); + $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + $plural_label = $entity_type->getPluralLabel(); + + $permissions["view {$entity_type_id}"] = [ + 'title' => $this->t('View @type', [ + '@type' => $plural_label, + ]), + ]; + foreach ($bundles as $bundle_name => $bundle_info) { + $permissions["view {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + } + + return $permissions; + } + +} diff --git a/web/modules/contrib/entity/src/EntityPermissionProviderBase.php b/web/modules/contrib/entity/src/EntityPermissionProviderBase.php new file mode 100644 index 000000000..c48e45634 --- /dev/null +++ b/web/modules/contrib/entity/src/EntityPermissionProviderBase.php @@ -0,0 +1,233 @@ +entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $container->get('entity_type.bundle.info') + ); + } + + /** + * {@inheritdoc} + */ + public function buildPermissions(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); + $plural_label = $entity_type->getPluralLabel(); + + $permissions = []; + $permissions["administer {$entity_type_id}"] = [ + 'title' => $this->t('Administer @type', ['@type' => $plural_label]), + 'restrict access' => TRUE, + ]; + if ($entity_type->hasLinkTemplate('collection')) { + $permissions["access {$entity_type_id} overview"] = [ + 'title' => $this->t('Access the @type overview page', ['@type' => $plural_label]), + ]; + } + if ($has_owner && $entity_type->entityClassImplements(EntityPublishedInterface::class)) { + $permissions["view own unpublished {$entity_type_id}"] = [ + 'title' => $this->t('View own unpublished @type', [ + '@type' => $plural_label, + ]), + ]; + } + + // Generate the other permissions based on granularity. + if ($entity_type->getPermissionGranularity() === 'entity_type') { + $permissions += $this->buildEntityTypePermissions($entity_type); + } + else { + $permissions += $this->buildBundlePermissions($entity_type); + } + + return $this->processPermissions($permissions, $entity_type); + } + + /** + * Adds the provider and converts the titles to strings to allow sorting. + * + * @param array $permissions + * The array of permissions. + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return array + * An array of processed permissions. + */ + protected function processPermissions(array $permissions, EntityTypeInterface $entity_type) { + foreach ($permissions as $name => $permission) { + // Permissions are grouped by provider on admin/people/permissions. + $permissions[$name]['provider'] = $entity_type->getProvider(); + // TranslatableMarkup objects don't sort properly. + $permissions[$name]['title'] = (string) $permission['title']; + } + return $permissions; + } + + /** + * Builds permissions for the entity_type granularity. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return array + * The permissions. + */ + protected function buildEntityTypePermissions(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); + $singular_label = $entity_type->getSingularLabel(); + $plural_label = $entity_type->getPluralLabel(); + + $permissions = []; + $permissions["create {$entity_type_id}"] = [ + 'title' => $this->t('Create @type', [ + '@type' => $plural_label, + ]), + ]; + if ($has_owner) { + $permissions["update any {$entity_type_id}"] = [ + 'title' => $this->t('Update any @type', [ + '@type' => $singular_label, + ]), + ]; + $permissions["update own {$entity_type_id}"] = [ + 'title' => $this->t('Update own @type', [ + '@type' => $plural_label, + ]), + ]; + $permissions["delete any {$entity_type_id}"] = [ + 'title' => $this->t('Delete any @type', [ + '@type' => $singular_label, + ]), + ]; + $permissions["delete own {$entity_type_id}"] = [ + 'title' => $this->t('Delete own @type', [ + '@type' => $plural_label, + ]), + ]; + } + else { + $permissions["update {$entity_type_id}"] = [ + 'title' => $this->t('Update @type', [ + '@type' => $plural_label, + ]), + ]; + $permissions["delete {$entity_type_id}"] = [ + 'title' => $this->t('Delete @type', [ + '@type' => $plural_label, + ]), + ]; + } + + return $permissions; + } + + /** + * Builds permissions for the bundle granularity. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return array + * The permissions. + */ + protected function buildBundlePermissions(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); + $singular_label = $entity_type->getSingularLabel(); + $plural_label = $entity_type->getPluralLabel(); + + $permissions = []; + foreach ($bundles as $bundle_name => $bundle_info) { + $permissions["create {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Create @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + + if ($has_owner) { + $permissions["update any {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Update any @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $singular_label, + ]), + ]; + $permissions["update own {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Update own @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + $permissions["delete any {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Delete any @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $singular_label, + ]), + ]; + $permissions["delete own {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Delete own @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + } + else { + $permissions["update {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Update @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + $permissions["delete {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Delete @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + } + } + + return $permissions; + } + +} diff --git a/web/modules/contrib/entity/src/EntityPermissionProviderInterface.php b/web/modules/contrib/entity/src/EntityPermissionProviderInterface.php new file mode 100644 index 000000000..50f3ccaa9 --- /dev/null +++ b/web/modules/contrib/entity/src/EntityPermissionProviderInterface.php @@ -0,0 +1,23 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * Builds a list of permissions for the participating entity types. + * + * @return array + * The permissions. + */ + public function buildPermissions() { + $permissions = []; + /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ + foreach ($this->entityTypeManager->getDefinitions() as $entity_type) { + if ($entity_type->hasHandlerClass('permission_provider')) { + $permission_provider_class = $entity_type->getHandlerClass('permission_provider'); + $permission_provider = $this->entityTypeManager->createHandlerInstance($permission_provider_class, $entity_type); + $permissions += $permission_provider->buildPermissions($entity_type); + } + } + + return $permissions; + } + +} diff --git a/web/modules/contrib/entity/src/EntityViewBuilder.php b/web/modules/contrib/entity/src/EntityViewBuilder.php new file mode 100644 index 000000000..1efe0ebe6 --- /dev/null +++ b/web/modules/contrib/entity/src/EntityViewBuilder.php @@ -0,0 +1,17 @@ +dateFormatter = $date_formatter; + $this->bundleInformation = $bundle_information; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('date.formatter'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'entity_revision_revert_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + if ($this->revision instanceof RevisionLogInterface) { + return $this->t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]); + } + return $this->t('Are you sure you want to revert the revision?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + if ($this->revision->getEntityType()->hasLinkTemplate('version-history')) { + return $this->revision->toUrl('version-history'); + } + return $this->revision->toUrl(); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Revert'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $_entity_revision = NULL, Request $request = NULL) { + $this->revision = $_entity_revision; + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // The revision timestamp will be updated when the revision is saved. Keep + // the original one for the confirmation message. + $this->revision = $this->prepareRevision($this->revision); + if ($this->revision instanceof RevisionLogInterface) { + $original_revision_timestamp = $this->revision->getRevisionCreationTime(); + + $this->revision->setRevisionLogMessage($this->t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)])); + $this->messenger()->addStatus(t('@type %title has been reverted to the revision from %revision-date.', [ + '@type' => $this->getBundleLabel($this->revision), + '%title' => $this->revision->label(), + '%revision-date' => $this->dateFormatter->format($original_revision_timestamp), + ])); + } + else { + $this->messenger()->addStatus(t('@type %title has been reverted', [ + '@type' => $this->getBundleLabel($this->revision), + '%title' => $this->revision->label(), + ])); + } + + $this->revision->save(); + + $this->logger('content')->notice('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]); + $form_state->setRedirect( + "entity.{$this->revision->getEntityTypeId()}.version_history", + [$this->revision->getEntityTypeId() => $this->revision->id()] + ); + } + + /** + * Prepares a revision to be reverted. + * + * @param \Drupal\Core\Entity\RevisionableInterface $revision + * The revision to be reverted. + * + * @return \Drupal\Core\Entity\RevisionableInterface + * The prepared revision ready to be stored. + */ + protected function prepareRevision(RevisionableInterface $revision) { + $revision->setNewRevision(); + $revision->isDefaultRevision(TRUE); + + return $revision; + } + + /** + * Returns a bundle label. + * + * @param \Drupal\Core\Entity\RevisionableInterface $revision + * The entity revision. + * + * @return string + */ + protected function getBundleLabel(RevisionableInterface $revision) { + /** @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface $revision */ + $bundle_info = $this->bundleInformation->getBundleInfo($revision->getEntityTypeId()); + return $bundle_info[$revision->bundle()]['label']; + } + +} diff --git a/web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php b/web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php new file mode 100644 index 000000000..6e7d8c7f4 --- /dev/null +++ b/web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php @@ -0,0 +1,165 @@ +getBundleEntity(); + + // Set up default values, if required. + if (!$this->entity->isNew()) { + $this->entity->setRevisionLogMessage(NULL); + } + + if ($bundle_entity instanceof RevisionableEntityBundleInterface) { + // Always use the default revision setting. + $this->entity->setNewRevision($bundle_entity && $bundle_entity->shouldCreateNewRevision()); + } + } + + /** + * Gets the bundle entity of the current entity. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The bundle entity, or NULL if there is none. + */ + protected function getBundleEntity() { + if ($this->entity->getEntityType()->getBundleEntityType()) { + $bundle_key = $this->entity->getEntityType()->getKey('bundle'); + return $this->entity->{$bundle_key}->referencedEntities()[0]; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $entity_type = $this->entity->getEntityType(); + $bundle_entity = $this->getBundleEntity(); + $account = $this->currentUser(); + + if ($this->operation == 'edit') { + $form['#title'] = $this->t('Edit %bundle_label @label', [ + '%bundle_label' => $bundle_entity ? $bundle_entity->label() : '', + '@label' => $this->entity->label(), + ]); + } + + $form['advanced'] = [ + '#type' => 'vertical_tabs', + '#weight' => 99, + ]; + + // Add a log field if the "Create new revision" option is checked, or if the + // current user has the ability to check that option. + // @todo Could we autogenerate this form by using some widget on the + // revision info field. + $form['revision_information'] = [ + '#type' => 'details', + '#title' => $this->t('Revision information'), + // Open by default when "Create new revision" is checked. + '#open' => $this->entity->isNewRevision(), + '#group' => 'advanced', + '#weight' => 20, + '#access' => $this->entity->isNewRevision() || $account->hasPermission($entity_type->get('admin_permission')), + ]; + + $form['revision_information']['revision'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Create new revision'), + '#default_value' => $this->entity->isNewRevision(), + '#access' => $account->hasPermission($entity_type->get('admin_permission')), + ]; + + // Check the revision log checkbox when the log textarea is filled in. + // This must not happen if "Create new revision" is enabled by default, + // since the state would auto-disable the checkbox otherwise. + if (!$this->entity->isNewRevision()) { + $form['revision_information']['revision']['#states'] = [ + 'checked' => [ + 'textarea[name="revision_log"]' => ['empty' => FALSE], + ], + ]; + } + + $form['revision_information']['revision_log'] = [ + '#type' => 'textarea', + '#title' => $this->t('Revision log message'), + '#rows' => 4, + '#default_value' => $this->entity->getRevisionLogMessage(), + '#description' => $this->t('Briefly describe the changes you have made.'), + ]; + + return parent::form($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + // Save as a new revision if requested to do so. + if (!$form_state->isValueEmpty('revision')) { + $this->entity->setNewRevision(); + } + + $insert = $this->entity->isNew(); + $this->entity->save(); + $context = ['@type' => $this->entity->bundle(), '%info' => $this->entity->label()]; + $logger = $this->logger('content'); + $bundle_entity = $this->getBundleEntity(); + $t_args = ['@type' => $bundle_entity ? $bundle_entity->label() : 'None', '%info' => $this->entity->label()]; + + if ($insert) { + $logger->notice('@type: added %info.', $context); + drupal_set_message($this->t('@type %info has been created.', $t_args)); + } + else { + $logger->notice('@type: updated %info.', $context); + drupal_set_message($this->t('@type %info has been updated.', $t_args)); + } + + if ($this->entity->id()) { + $form_state->setValue('id', $this->entity->id()); + $form_state->set('id', $this->entity->id()); + + if ($this->entity->getEntityType()->hasLinkTemplate('collection')) { + $form_state->setRedirectUrl($this->entity->toUrl('collection')); + } + else { + $form_state->setRedirectUrl($this->entity->toUrl('canonical')); + } + } + else { + // In the unlikely case something went wrong on save, the entity will be + // rebuilt and entity form redisplayed. + drupal_set_message($this->t('The entity could not be saved.'), 'error'); + $form_state->setRebuild(); + } + } + +} diff --git a/web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php b/web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php new file mode 100644 index 000000000..8b7a04e7a --- /dev/null +++ b/web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php @@ -0,0 +1,81 @@ +entityType = $entity_type; + $this->setStringTranslation($string_translation); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + /* @var \Drupal\Core\Entity\EntityTypeManagerInterface */ + $entity_type_manager = $container->get('entity_type.manager'); + // The plugin ID is of the form + // "entity.entity_actions:entity.$entity_type_id.collection". + // @see entity.links.action.yml + // @see \Drupal\entity\Menu\EntityCollectionLocalActionProvider::buildLocalActions() + list(, $derivate_id) = explode(':', $plugin_id); + list(, $entity_type_id, ) = explode('.', $derivate_id); + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('router.route_provider'), + $entity_type_manager->getDefinition($entity_type_id), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getTitle(Request $request = NULL) { + return (string) $this->t('Add @entity', [ + '@entity' => (string) $this->entityType->getSingularLabel(), + ]); + } + +} diff --git a/web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php b/web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php new file mode 100644 index 000000000..4ddd554f6 --- /dev/null +++ b/web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php @@ -0,0 +1,47 @@ +hasLinkTemplate('collection')) { + $entity_type_id = $entity_type->id(); + + if ($entity_type->hasLinkTemplate('add-page')) { + $route_name = "entity.$entity_type_id.add_page"; + } + elseif ($entity_type->hasLinkTemplate('add-form')) { + $route_name = "entity.$entity_type_id.add_form"; + } + + if (isset($route_name)) { + $actions[$route_name] = [ + // The title is translated at runtime by EntityAddLocalAction. + /* @see \Drupal\entity\Menu\EntityAddLocalAction::getTitle() */ + 'title' => 'Add ' . $entity_type->getSingularLabel(), + 'route_name' => $route_name, + 'options' => [ + // Redirect back to the collection after form submission. + 'query' => [ + 'destination' => $entity_type->getLinkTemplate('collection'), + ], + ], + 'appears_on' => ["entity.$entity_type_id.collection"], + 'class' => EntityAddLocalAction::class, + ]; + } + } + return $actions; + } + +} diff --git a/web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php b/web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php new file mode 100644 index 000000000..bba16c328 --- /dev/null +++ b/web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php @@ -0,0 +1,23 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static($container->get('entity_type.manager')); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + if (empty($this->derivatives)) { + $definitions = []; + foreach ($this->getParticipatingEntityTypes() as $entity_type_id => $entity_type) { + $definition = $base_plugin_definition; + $definition['label'] = t('Delete @entity_type (Deprecated)', ['@entity_type' => $entity_type->getSingularLabel()]); + $definition['type'] = $entity_type_id; + $definition['confirm_form_route_name'] = 'entity.' . $entity_type_id . '.delete_multiple_form'; + $definitions[$entity_type_id] = $definition; + } + $this->derivatives = $definitions; + } + + return parent::getDerivativeDefinitions($base_plugin_definition); + } + + /** + * Gets a list of participating entity types. + * + * The list consists of all content entity types with a delete-multiple-form + * link template. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * The participating entity types, keyed by entity type id. + */ + protected function getParticipatingEntityTypes() { + $entity_types = $this->entityTypeManager->getDefinitions(); + $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { + // Core requires a "delete-multiple-confirm" form to be declared as well, + // if it's missing, it's safe to assume that the entity type is still + // relying on previous Entity API contrib behavior. + return $entity_type->hasLinkTemplate('delete-multiple-form') && !$entity_type->hasHandlerClass('form', 'delete-multiple-confirm'); + }); + + return $entity_types; + } + +} diff --git a/web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php b/web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php new file mode 100644 index 000000000..568ad7e87 --- /dev/null +++ b/web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php @@ -0,0 +1,58 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static($container->get('entity_type.manager')); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + if (!$this->derivatives) { + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + $handlers = $entity_type->getHandlerClasses(); + if (isset($handlers['local_action_provider'])) { + foreach ($handlers['local_action_provider'] as $class) { + /** @var \Drupal\entity\Menu\EntityLocalActionProviderInterface $handler */ + $handler = $this->entityTypeManager->createHandlerInstance($class, $entity_type); + $this->derivatives += $handler->buildLocalActions($entity_type); + } + } + } + } + return $this->derivatives; + } + +} diff --git a/web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php b/web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php new file mode 100644 index 000000000..b00b8ceb4 --- /dev/null +++ b/web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php @@ -0,0 +1,68 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $exclude = ['node']; + + $this->derivatives = []; + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + if (in_array($entity_type_id, $exclude)) { + continue; + } + + if (!$entity_type->hasLinkTemplate('version-history')) { + continue; + } + + $this->derivatives[$entity_type_id] = [ + 'route_name' => "entity.$entity_type_id.version_history", + 'title' => t('Revisions'), + 'base_route' => "entity.$entity_type_id.canonical", + 'weight' => 20, + ] + $base_plugin_definition; + } + + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/web/modules/contrib/entity/src/QueryAccess/Condition.php b/web/modules/contrib/entity/src/QueryAccess/Condition.php new file mode 100644 index 000000000..ef1f630c3 --- /dev/null +++ b/web/modules/contrib/entity/src/QueryAccess/Condition.php @@ -0,0 +1,113 @@ +', '<', '<=', '>', '>=', 'BETWEEN', 'NOT BETWEEN', + 'IN', 'NOT IN', 'IS NULL', 'IS NOT NULL', + ]; + + /** + * The field. + * + * @var string + */ + protected $field; + + /** + * The value. + * + * @var mixed + */ + protected $value; + + /** + * The operator. + * + * @var string + */ + protected $operator; + + /** + * Constructs a new Condition object. + * + * @param string $field + * The field, with an optional column name. E.g: 'uid', 'address.locality'. + * @param mixed $value + * The value. + * @param string $operator + * The operator. + * Possible values: =, <>, <, <=, >, >=, BETWEEN, NOT BETWEEN, + * IN, NOT IN, IS NULL, IS NOT NULL. + */ + public function __construct($field, $value, $operator = NULL) { + // Provide a default based on the data type of the value. + if (!isset($operator)) { + $operator = is_array($value) ? 'IN' : '='; + } + // Validate the selected operator. + if (!in_array($operator, self::$supportedOperators)) { + throw new \InvalidArgumentException(sprintf('Unrecognized operator "%s".', $operator)); + } + + $this->field = $field; + $this->value = $value; + $this->operator = $operator; + } + + /** + * {@inheritdoc} + */ + public function getField() { + return $this->field; + } + + /** + * {@inheritdoc} + */ + public function getValue() { + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function getOperator() { + return $this->operator; + } + + /** + * Gets the string representation of the condition. + * + * Used for debugging purposes. + * + * @return string + * The string representation of the condition. + */ + public function __toString() { + if (in_array($this->operator, ['IS NULL', 'IS NOT NULL'])) { + return "{$this->field} {$this->operator}"; + } + else { + if (is_array($this->value)) { + $value = "['" . implode("', '", $this->value) . "']"; + } + else { + $value = "'" . $this->value . "'"; + } + + return "{$this->field} {$this->operator} $value"; + } + } + +} diff --git a/web/modules/contrib/entity/src/QueryAccess/ConditionGroup.php b/web/modules/contrib/entity/src/QueryAccess/ConditionGroup.php new file mode 100644 index 000000000..094feb109 --- /dev/null +++ b/web/modules/contrib/entity/src/QueryAccess/ConditionGroup.php @@ -0,0 +1,227 @@ +addCondition('type', ['article', 'page']); + * $condition_group->addCondition('uid', '1'); + * + * // Filter by node type or status. + * $condition_group = new ConditionGroup('OR'); + * $condition_group->addCondition('type', ['article', 'page']); + * $condition_group->addCondition('status', '1', '<>'); + * + * // Nested condition groups: node type AND (uid OR status). + * $condition_group = new ConditionGroup(); + * $condition_group->addCondition('type', ['article', 'page']); + * $condition_group->addCondition((new ConditionGroup('OR')) + * ->addCondition('uid', 1) + * ->addCondition('status', '1') + * ); + * @endcode + */ +final class ConditionGroup implements \Countable, RefinableCacheableDependencyInterface { + + use RefinableCacheableDependencyTrait; + + /** + * The conjunction. + * + * @var string + */ + protected $conjunction; + + /** + * The conditions. + * + * @var \Drupal\entity\QueryAccess\Condition[]|\Drupal\entity\QueryAccess\ConditionGroup[] + */ + protected $conditions = []; + + /** + * Whether the condition group is always FALSE. + * + * @var bool + */ + protected $alwaysFalse = FALSE; + + /** + * Constructs a new ConditionGroup object. + * + * @param string $conjunction + * The conjunction. + */ + public function __construct($conjunction = 'AND') { + $this->conjunction = $conjunction; + } + + /** + * Gets the conjunction. + * + * @return string + * The conjunction. Possible values: AND, OR. + */ + public function getConjunction() { + return $this->conjunction; + } + + /** + * Gets all conditions and nested condition groups. + * + * @return \Drupal\entity\QueryAccess\Condition[]|\Drupal\entity\QueryAccess\ConditionGroup[] + * The conditions, where each one is either a Condition or a nested + * ConditionGroup. Returned by reference, to allow callers to replace + * or remove conditions. + */ + public function &getConditions() { + return $this->conditions; + } + + /** + * Adds a condition. + * + * @param string|\Drupal\entity\QueryAccess\ConditionGroup $field + * Either a condition group (for nested AND/OR conditions), or a + * field name with an optional column name. E.g: 'uid', 'address.locality'. + * @param mixed $value + * The value. + * @param string $operator + * The operator. + * Possible values: =, <>, <, <=, >, >=, BETWEEN, NOT BETWEEN, + * IN, NOT IN, IS NULL, IS NOT NULL. + * + * @return $this + */ + public function addCondition($field, $value = NULL, $operator = NULL) { + if ($field instanceof ConditionGroup) { + if ($field->count() === 1) { + // The condition group only has a single condition, merge it. + $this->conditions[] = reset($field->getConditions()); + $this->addCacheTags($field->getCacheTags()); + $this->addCacheContexts($field->getCacheContexts()); + $this->mergeCacheMaxAge($field->getCacheMaxAge()); + } + elseif ($field->count() > 1) { + $this->conditions[] = $field; + } + } + else { + $this->conditions[] = new Condition($field, $value, $operator); + } + + return $this; + } + + /** + * Gets whether the condition group is always FALSE. + * + * Used when the user doesn't have access to any entities, to ensure that a + * query returns no results. + * + * @return bool + * Whether the condition group is always FALSE. + */ + public function isAlwaysFalse() { + return $this->alwaysFalse; + } + + /** + * Sets whether the condition group should always be FALSE. + * + * @param bool $always_false + * Whether the condition group should always be FALSE. + * + * @return $this + */ + public function alwaysFalse($always_false = TRUE) { + $this->alwaysFalse = $always_false; + return $this; + } + + /** + * Clones the contained conditions when the condition group is cloned. + */ + public function __clone() { + foreach ($this->conditions as $i => $condition) { + $this->conditions[$i] = clone $condition; + } + } + + /** + * Gets the string representation of the condition group. + * + * @return string + * The string representation of the condition group. + */ + public function __toString() { + // Special case for a single, nested condition group: + if (count($this->conditions) == 1) { + return (string) reset($this->conditions); + } + $lines = []; + foreach ($this->conditions as $condition) { + $lines[] = str_replace("\n", "\n ", (string) $condition); + } + return $lines ? "(\n " . implode("\n {$this->conjunction}\n ", $lines) . "\n)" : ''; + } + + /** + * {@inheritdoc} + */ + public function count() { + return count($this->conditions); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + $tags = $this->cacheTags; + foreach ($this->conditions as $condition) { + if ($condition instanceof ConditionGroup) { + $tags = array_merge($tags, $condition->getCacheTags()); + } + } + return Cache::mergeTags($tags, []); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + $cache_contexts = $this->cacheContexts; + foreach ($this->conditions as $condition) { + if ($condition instanceof ConditionGroup) { + $cache_contexts = array_merge($cache_contexts, $condition->getCacheContexts()); + } + } + return Cache::mergeContexts($cache_contexts); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + $max_age = $this->cacheMaxAge; + foreach ($this->conditions as $condition) { + if ($condition instanceof ConditionGroup) { + $max_age = Cache::mergeMaxAges($max_age, $condition->getCacheMaxAge()); + } + } + return $max_age; + } + +} diff --git a/web/modules/contrib/entity/src/QueryAccess/EntityQueryAlter.php b/web/modules/contrib/entity/src/QueryAccess/EntityQueryAlter.php new file mode 100644 index 000000000..25c7395c8 --- /dev/null +++ b/web/modules/contrib/entity/src/QueryAccess/EntityQueryAlter.php @@ -0,0 +1,181 @@ +entityFieldManager = $entity_field_manager; + $this->entityTypeManager = $entity_type_manager; + $this->renderer = $renderer; + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_field.manager'), + $container->get('entity_type.manager'), + $container->get('renderer'), + $container->get('request_stack') + ); + } + + /** + * Alters the select query for the given entity type. + * + * @param \Drupal\Core\Database\Query\SelectInterface $query + * The select query. + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + */ + public function alter(SelectInterface $query, EntityTypeInterface $entity_type) { + if (!$entity_type->hasHandlerClass('query_access')) { + return; + } + $entity_type_id = $entity_type->id(); + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if (!$storage instanceof SqlContentEntityStorage) { + return; + } + + /** @var \Drupal\entity\QueryAccess\QueryAccessHandlerInterface $query_access */ + $query_access = $this->entityTypeManager->getHandler($entity_type_id, 'query_access'); + $conditions = $query_access->getConditions('view'); + if ($conditions->isAlwaysFalse()) { + $query->where('1 = 0'); + } + elseif (count($conditions)) { + $sql_conditions = $this->mapConditions($conditions, $query); + $query->condition($sql_conditions); + } + + $this->applyCacheability(CacheableMetadata::createFromObject($conditions)); + } + + /** + * Maps an entity type's access conditions to SQL conditions. + * + * @param \Drupal\entity\QueryAccess\ConditionGroup $conditions + * The access conditions. + * @param \Drupal\Core\Database\Query\SelectInterface $query + * The SQL query. + * @param bool $nested_inside_or + * Whether the access conditions are nested inside an OR condition. + * + * @return \Drupal\Core\Database\Query\ConditionInterface + * The SQL conditions. + */ + protected function mapConditions(ConditionGroup $conditions, SelectInterface $query, $nested_inside_or = FALSE) { + $sql_condition = $query->conditionGroupFactory($conditions->getConjunction()); + $tables = new Tables($query); + $nested_inside_or = $nested_inside_or || $conditions->getConjunction() == 'OR'; + foreach ($conditions->getConditions() as $condition) { + if ($condition instanceof ConditionGroup) { + $nested_sql_conditions = $this->mapConditions($condition, $query, $nested_inside_or); + $sql_condition->condition($nested_sql_conditions); + } + else { + // Access conditions don't specify a langcode. + $langcode = NULL; + $type = $nested_inside_or || $condition->getOperator() === 'IS NULL' ? 'LEFT' : 'INNER'; + $sql_field = $tables->addField($condition->getField(), $type, $langcode); + $value = $condition->getValue(); + $operator = $condition->getOperator(); + // Using LIKE/NOT LIKE ensures a case insensitive comparison. + // @see \Drupal\Core\Entity\Query\Sql\Condition::translateCondition(). + $case_sensitive = $tables->isFieldCaseSensitive($condition->getField()); + $operator_map = [ + '=' => 'LIKE', + '<>' => 'NOT LIKE', + ]; + if (!$case_sensitive && isset($operator_map[$operator])) { + $operator = $operator_map[$operator]; + $value = $query->escapeLike($value); + } + + $sql_condition->condition($sql_field, $value, $operator); + } + } + + return $sql_condition; + } + + /** + * Applies the cacheablity metadata to the current request. + * + * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata + * The cacheability metadata. + */ + protected function applyCacheability(CacheableMetadata $cacheable_metadata) { + $request = $this->requestStack->getCurrentRequest(); + if ($request->isMethodCacheable() && $this->renderer->hasRenderContext()) { + $build = []; + $cacheable_metadata->applyTo($build); + $this->renderer->render($build); + } + } + +} diff --git a/web/modules/contrib/entity/src/QueryAccess/QueryAccessEvent.php b/web/modules/contrib/entity/src/QueryAccess/QueryAccessEvent.php new file mode 100644 index 000000000..979010bfb --- /dev/null +++ b/web/modules/contrib/entity/src/QueryAccess/QueryAccessEvent.php @@ -0,0 +1,92 @@ +conditions = $conditions; + $this->operation = $operation; + $this->account = $account; + } + + /** + * Gets the conditions. + * + * If $conditions->isAlwaysFalse() is TRUE, the user doesn't have access to + * any entities, and the query is expected to return no results. + * This can be reversed by calling $conditions->alwaysFalse(FALSE). + * + * If $conditions->isAlwaysFalse() is FALSE, and the condition group is + * empty (count is 0), the user has full access, and the query doesn't + * need to be restricted. + * + * @return \Drupal\entity\QueryAccess\ConditionGroup + * The conditions. + */ + public function getConditions() { + return $this->conditions; + } + + /** + * Gets the operation. + * + * @return string + * The operation. Usually one of "view", "update" or "delete". + */ + public function getOperation() { + return $this->operation; + } + + /** + * Gets the user for which to restrict access. + * + * @return \Drupal\Core\Session\AccountInterface + * The user. + */ + public function getAccount() { + return $this->account; + } + +} diff --git a/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandler.php b/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandler.php new file mode 100644 index 000000000..f9ec51c4c --- /dev/null +++ b/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandler.php @@ -0,0 +1,27 @@ +buildEntityConditions($operation, $account); + } + + return parent::buildEntityOwnerConditions($operation, $account); + } + +} diff --git a/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerBase.php b/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerBase.php new file mode 100644 index 000000000..aaa131539 --- /dev/null +++ b/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerBase.php @@ -0,0 +1,268 @@ +entityType = $entity_type; + $this->bundleInfo = $bundle_info; + $this->eventDispatcher = $event_dispatcher; + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity_type.bundle.info'), + $container->get('event_dispatcher'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function getConditions($operation, AccountInterface $account = NULL) { + $account = $account ?: $this->currentUser; + $entity_type_id = $this->entityType->id(); + $conditions = $this->buildConditions($operation, $account); + + // Allow other modules to modify the conditions before they are used. + $event = new QueryAccessEvent($conditions, $operation, $account); + $this->eventDispatcher->dispatch("entity.query_access.{$entity_type_id}", $event); + + return $conditions; + } + + /** + * Builds the conditions for the given operation and user. + * + * @param string $operation + * The access operation. Usually one of "view", "update" or "delete". + * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to restrict access. + * + * @return \Drupal\entity\QueryAccess\ConditionGroup + * The conditions. + */ + public function buildConditions($operation, AccountInterface $account) { + $entity_type_id = $this->entityType->id(); + $has_owner = $this->entityType->entityClassImplements(EntityOwnerInterface::class); + + if ($account->hasPermission("administer {$entity_type_id}")) { + // The user has full access to all operations, no conditions needed. + $conditions = new ConditionGroup('OR'); + $conditions->addCacheContexts(['user.permissions']); + return $conditions; + } + + if ($has_owner) { + $entity_conditions = $this->buildEntityOwnerConditions($operation, $account); + } + else { + $entity_conditions = $this->buildEntityConditions($operation, $account); + } + + $conditions = NULL; + if ($operation == 'view' && $this->entityType->entityClassImplements(EntityPublishedInterface::class)) { + $uid_key = $this->entityType->getKey('uid'); + $published_key = $this->entityType->getKey('published'); + $published_conditions = NULL; + $unpublished_conditions = NULL; + + if ($entity_conditions) { + // Restrict the existing conditions to published entities only. + $published_conditions = new ConditionGroup('AND'); + $published_conditions->addCacheContexts(['user.permissions']); + $published_conditions->addCondition($entity_conditions); + $published_conditions->addCondition($published_key, '1'); + } + if ($has_owner && $account->hasPermission("view own unpublished $entity_type_id")) { + $unpublished_conditions = new ConditionGroup('AND'); + $unpublished_conditions->addCacheContexts(['user']); + $unpublished_conditions->addCondition($uid_key, $account->id()); + $unpublished_conditions->addCondition($published_key, '0'); + } + + if ($published_conditions && $unpublished_conditions) { + $conditions = new ConditionGroup('OR'); + $conditions->addCondition($published_conditions); + $conditions->addCondition($unpublished_conditions); + } + elseif ($published_conditions) { + $conditions = $published_conditions; + } + elseif ($unpublished_conditions) { + $conditions = $unpublished_conditions; + } + } + else { + $conditions = $entity_conditions; + } + + if (!$conditions) { + // The user doesn't have access to any entities. + // Falsify the query to ensure no results are returned. + $conditions = new ConditionGroup('OR'); + $conditions->addCacheContexts(['user.permissions']); + $conditions->alwaysFalse(); + } + + return $conditions; + } + + /** + * Builds the conditions for entities that have an owner. + * + * @param string $operation + * The access operation. Usually one of "view", "update" or "delete". + * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to restrict access. + * + * @return \Drupal\entity\QueryAccess\ConditionGroup|null + * The conditions, or NULL if the user doesn't have access to any entity. + */ + protected function buildEntityOwnerConditions($operation, AccountInterface $account) { + $entity_type_id = $this->entityType->id(); + $uid_key = $this->entityType->getKey('uid'); + $bundle_key = $this->entityType->getKey('bundle'); + + $conditions = new ConditionGroup('OR'); + $conditions->addCacheContexts(['user.permissions']); + // Any $entity_type permission. + if ($account->hasPermission("$operation any $entity_type_id")) { + // The user has full access, no conditions needed. + return $conditions; + } + + // Own $entity_type permission. + if ($account->hasPermission("$operation own $entity_type_id")) { + $conditions->addCacheContexts(['user']); + $conditions->addCondition($uid_key, $account->id()); + } + + $bundles = array_keys($this->bundleInfo->getBundleInfo($entity_type_id)); + $bundles_with_any_permission = []; + $bundles_with_own_permission = []; + foreach ($bundles as $bundle) { + if ($account->hasPermission("$operation any $bundle $entity_type_id")) { + $bundles_with_any_permission[] = $bundle; + } + if ($account->hasPermission("$operation own $bundle $entity_type_id")) { + $bundles_with_own_permission[] = $bundle; + } + } + // Any $bundle permission. + if ($bundles_with_any_permission) { + $conditions->addCondition($bundle_key, $bundles_with_any_permission); + } + // Own $bundle permission. + if ($bundles_with_own_permission) { + $conditions->addCacheContexts(['user']); + $conditions->addCondition((new ConditionGroup('AND')) + ->addCondition($uid_key, $account->id()) + ->addCondition($bundle_key, $bundles_with_own_permission) + ); + } + + return $conditions->count() ? $conditions : NULL; + } + + /** + * Builds the conditions for entities that do not have an owner. + * + * @param string $operation + * The access operation. Usually one of "view", "update" or "delete". + * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to restrict access. + * + * @return \Drupal\entity\QueryAccess\ConditionGroup|null + * The conditions, or NULL if the user doesn't have access to any entity. + */ + protected function buildEntityConditions($operation, AccountInterface $account) { + $entity_type_id = $this->entityType->id(); + $bundle_key = $this->entityType->getKey('bundle'); + + $conditions = new ConditionGroup('OR'); + $conditions->addCacheContexts(['user.permissions']); + // The $entity_type permission. + if ($account->hasPermission("$operation $entity_type_id")) { + // The user has full access, no conditions needed. + return $conditions; + } + + $bundles = array_keys($this->bundleInfo->getBundleInfo($entity_type_id)); + $bundles_with_any_permission = []; + foreach ($bundles as $bundle) { + if ($account->hasPermission("$operation $bundle $entity_type_id")) { + $bundles_with_any_permission[] = $bundle; + } + } + // The $bundle permission. + if ($bundles_with_any_permission) { + $conditions->addCondition($bundle_key, $bundles_with_any_permission); + } + + return $conditions->count() ? $conditions : NULL; + } + +} diff --git a/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerInterface.php b/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerInterface.php new file mode 100644 index 000000000..15d455a62 --- /dev/null +++ b/web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerInterface.php @@ -0,0 +1,41 @@ +connection = $connection; + $this->entityFieldManager = $entity_field_manager; + $this->entityTypeManager = $entity_type_manager; + $this->renderer = $renderer; + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('database'), + $container->get('entity_field.manager'), + $container->get('entity_type.manager'), + $container->get('renderer'), + $container->get('request_stack') + ); + } + + /** + * Alters the given views query. + * + * @param \Drupal\views\Plugin\views\query\Sql $query + * The views query. + * @param \Drupal\views\ViewExecutable $view + * The view. + */ + public function alter(Sql $query, ViewExecutable $view) { + $table_info = $query->getEntityTableInfo(); + $base_table = reset($table_info); + if (empty($base_table['entity_type']) || $base_table['relationship_id'] != 'none') { + return; + } + $entity_type_id = $base_table['entity_type']; + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + if (!$entity_type->hasHandlerClass('query_access')) { + return; + } + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if (!$storage instanceof SqlContentEntityStorage) { + return; + } + + /** @var \Drupal\entity\QueryAccess\QueryAccessHandlerInterface $query_access */ + $query_access = $this->entityTypeManager->getHandler($entity_type_id, 'query_access'); + $conditions = $query_access->getConditions('view'); + if ($conditions->isAlwaysFalse()) { + $query->addWhereExpression(0, '1 = 0'); + } + elseif (count($conditions)) { + // Store the data table, in case mapConditions() needs to join it in. + $base_table['data_table'] = $entity_type->getDataTable(); + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $sql_conditions = $this->mapConditions($conditions, $query, $base_table, $field_storage_definitions, $table_mapping); + $query->addWhere(0, $sql_conditions); + } + + $this->applyCacheability(CacheableMetadata::createFromObject($conditions)); + } + + /** + * Maps an entity type's access conditions to views SQL conditions. + * + * @param \Drupal\entity\QueryAccess\ConditionGroup $conditions + * The access conditions. + * @param \Drupal\views\Plugin\views\query\Sql $query + * The views query. + * @param array $base_table + * The base table information. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions + * The field storage definitions. + * @param \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping + * The table mapping. + * + * @return \Drupal\Core\Database\Query\ConditionInterface + * The SQL conditions. + */ + protected function mapConditions(ConditionGroup $conditions, Sql $query, array $base_table, array $field_storage_definitions, DefaultTableMapping $table_mapping) { + $sql_condition = new SqlCondition($conditions->getConjunction()); + foreach ($conditions->getConditions() as $condition) { + if ($condition instanceof ConditionGroup) { + $nested_sql_conditions = $this->mapConditions($condition, $query, $base_table, $field_storage_definitions, $table_mapping); + $sql_condition->condition($nested_sql_conditions); + } + else { + $field = $condition->getField(); + $property_name = NULL; + if (strpos($field, '.') !== FALSE) { + list($field, $property_name) = explode('.', $field); + } + // Skip unknown fields. + if (!isset($field_storage_definitions[$field])) { + continue; + } + $field_storage_definition = $field_storage_definitions[$field]; + if (!$property_name) { + $property_name = $field_storage_definition->getMainPropertyName(); + } + + $column = $table_mapping->getFieldColumnName($field_storage_definition, $property_name); + if ($table_mapping->requiresDedicatedTableStorage($field_storage_definitions[$field])) { + if ($base_table['revision']) { + $dedicated_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition); + } + else { + $dedicated_table = $table_mapping->getDedicatedDataTableName($field_storage_definition); + } + // Views defaults to LEFT JOIN. For simplicity, we don't try to + // use an INNER JOIN when it's safe to do so (AND conjunctions). + $alias = $query->ensureTable($dedicated_table); + } + elseif ($base_table['revision'] && !$field_storage_definition->isRevisionable()) { + // Workaround for #2652652, which causes $query->ensureTable() + // to not work in this case, due to a missing relationship. + if ($data_table = $query->getTableInfo($base_table['data_table'])) { + $alias = $data_table['alias']; + } + else { + $configuration = [ + 'type' => 'INNER', + 'table' => $base_table['data_table'], + 'field' => 'id', + 'left_table' => $base_table['alias'], + 'left_field' => 'id', + ]; + /** @var \Drupal\Views\Plugin\views\join\JoinPluginBase $join */ + $join = Views::pluginManager('join')->createInstance('standard', $configuration); + $alias = $query->addRelationship($base_table['data_table'], $join, $data_table); + } + } + else { + $alias = $base_table['alias']; + } + + $value = $condition->getValue(); + $operator = $condition->getOperator(); + // Using LIKE/NOT LIKE ensures a case insensitive comparison. + // @see \Drupal\Core\Entity\Query\Sql\Condition::translateCondition(). + $property_definitions = $field_storage_definition->getPropertyDefinitions(); + $case_sensitive = $property_definitions[$property_name]->getSetting('case_sensitive'); + $operator_map = [ + '=' => 'LIKE', + '<>' => 'NOT LIKE', + ]; + if (!$case_sensitive && isset($operator_map[$operator])) { + $operator = $operator_map[$operator]; + $value = $this->connection->escapeLike($value); + } + + $sql_condition->condition("$alias.$column", $value, $operator); + } + } + + return $sql_condition; + } + + /** + * Applies the cacheablity metadata to the current request. + * + * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata + * The cacheability metadata. + */ + protected function applyCacheability(CacheableMetadata $cacheable_metadata) { + $request = $this->requestStack->getCurrentRequest(); + if ($request->isMethodCacheable() && $this->renderer->hasRenderContext()) { + $build = []; + $cacheable_metadata->applyTo($build); + $this->renderer->render($build); + } + } + +} diff --git a/web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php b/web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php new file mode 100644 index 000000000..f6dc11cb3 --- /dev/null +++ b/web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php @@ -0,0 +1,25 @@ +getEntityType()->getLinkTemplate($rel), $this->getEntityTypeId() . '_revision') !== FALSE) { + $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); + } + + return $uri_route_parameters; + } + +} diff --git a/web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php b/web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php new file mode 100644 index 000000000..7ef2f6e18 --- /dev/null +++ b/web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php @@ -0,0 +1,26 @@ +hasHandlerClass('permission_provider')) { + $admin_permission = $entity_type->getAdminPermission(); + $overview_permission = "access {$entity_type->id()} overview"; + $route->setRequirement('_permission', "$admin_permission+$overview_permission"); + } + return $route; + } + +} diff --git a/web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php b/web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php new file mode 100644 index 000000000..06a350400 --- /dev/null +++ b/web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php @@ -0,0 +1,26 @@ +hasHandlerClass('permission_provider')) { + $admin_permission = $entity_type->getAdminPermission(); + $overview_permission = "access {$entity_type->id()} overview"; + $route->setRequirement('_permission', "$admin_permission+$overview_permission"); + } + return $route; + } + +} diff --git a/web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php b/web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php new file mode 100644 index 000000000..7fb95481a --- /dev/null +++ b/web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php @@ -0,0 +1,54 @@ +deleteMultipleFormRoute($entity_type)) { + $routes->add('entity.' . $entity_type->id() . '.delete_multiple_form', $route); + } + + return $routes; + } + + /** + * Returns the delete multiple form route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function deleteMultipleFormRoute(EntityTypeInterface $entity_type) { + // Core requires a "delete-multiple-confirm" form to be declared as well, + // if it's missing, it's safe to assume that the entity type is still + // relying on previous Entity API contrib behavior. + if ($entity_type->hasLinkTemplate('delete-multiple-form') && !$entity_type->hasHandlerClass('form', 'delete-multiple-confirm')) { + $route = new Route($entity_type->getLinkTemplate('delete-multiple-form')); + $route->setDefault('_form', '\Drupal\entity\Form\DeleteMultipleForm'); + $route->setDefault('entity_type_id', $entity_type->id()); + $route->setRequirement('_entity_delete_multiple_access', $entity_type->id()); + + return $route; + } + } + +} diff --git a/web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php b/web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php new file mode 100644 index 000000000..e3412958c --- /dev/null +++ b/web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php @@ -0,0 +1,127 @@ +id(); + if ($view_route = $this->getRevisionViewRoute($entity_type)) { + $collection->add("entity.$entity_type_id.revision", $view_route); + } + if ($view_route = $this->getRevisionRevertRoute($entity_type)) { + $collection->add("entity.$entity_type_id.revision_revert_form", $view_route); + } + + if ($view_route = $this->getRevisionHistoryRoute($entity_type)) { + $collection->add("entity.$entity_type_id.version_history", $view_route); + } + + return $collection; + } + + /** + * Gets the entity revision view route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getRevisionViewRoute(EntityTypeInterface $entity_type) { + if ($entity_type->hasLinkTemplate('revision')) { + $entity_type_id = $entity_type->id(); + $route = new Route($entity_type->getLinkTemplate('revision')); + $route->addDefaults([ + '_controller' => '\Drupal\Core\Entity\Controller\EntityViewController::viewRevision', + '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title', + ]); + $route->addRequirements([ + '_entity_access_revision' => "$entity_type_id.view", + ]); + $route->setOption('parameters', [ + $entity_type->id() => [ + 'type' => 'entity:' . $entity_type->id(), + ], + $entity_type->id() . '_revision' => [ + 'type' => 'entity_revision:' . $entity_type->id(), + ], + ]); + return $route; + } + } + + /** + * Gets the entity revision revert route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getRevisionRevertRoute(EntityTypeInterface $entity_type) { + if ($entity_type->hasLinkTemplate('revision-revert-form')) { + $entity_type_id = $entity_type->id(); + $route = new Route($entity_type->getLinkTemplate('revision-revert-form')); + $route->addDefaults([ + '_form' => '\Drupal\entity\Form\RevisionRevertForm', + 'title' => 'Revert to earlier revision', + ]); + $route->addRequirements([ + '_entity_access_revision' => "$entity_type_id.update", + ]); + $route->setOption('parameters', [ + $entity_type->id() => [ + 'type' => 'entity:' . $entity_type->id(), + ], + $entity_type->id() . '_revision' => [ + 'type' => 'entity_revision:' . $entity_type->id(), + ], + ]); + return $route; + } + } + + /** + * Gets the entity revision version history route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getRevisionHistoryRoute($entity_type) { + if ($entity_type->hasLinkTemplate('version-history')) { + $entity_type_id = $entity_type->id(); + $route = new Route($entity_type->getLinkTemplate('version-history')); + $route->addDefaults([ + '_controller' => '\Drupal\entity\Controller\RevisionOverviewController::revisionOverviewController', + '_title' => 'Revisions', + ]); + $route->setRequirement('_entity_access_revision', "$entity_type_id.list"); + $route->setOption('entity_type_id', $entity_type->id()); + $route->setOption('parameters', [ + $entity_type->id() => [ + 'type' => 'entity:' . $entity_type->id(), + ], + ]); + return $route; + } + } + +} diff --git a/web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php b/web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php new file mode 100644 index 000000000..a53724bed --- /dev/null +++ b/web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php @@ -0,0 +1,54 @@ +hasHandlerClass('permission_provider') || !is_a($entity_type->getHandlerClass('permission_provider'), UncacheableEntityPermissionProvider::class, TRUE)) { + throw new \Exception('\Drupal\entity\UncacheableEntityAccessControlHandler requires the \Drupal\entity\UncacheableEntityPermissionProvider permission provider.'); + } + } + + /** + * {@inheritdoc} + */ + protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\user\EntityOwnerInterface $entity */ + if ($operation === 'view' && $entity instanceof EntityPublishedInterface && !$entity->isPublished()) { + if ($account->id() != $entity->getOwnerId()) { + // There's no permission for viewing other user's unpublished entity. + return AccessResult::neutral()->cachePerUser(); + } + + $permissions = [ + "view own unpublished {$entity->getEntityTypeId()}", + ]; + $result = AccessResult::allowedIfHasPermissions($account, $permissions)->cachePerUser(); + } + else { + $result = parent::checkEntityOwnerPermissions($entity, $operation, $account); + } + + return $result; + } + +} diff --git a/web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php b/web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php new file mode 100644 index 000000000..1b25b2ad2 --- /dev/null +++ b/web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php @@ -0,0 +1,145 @@ +id(); + $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); + $plural_label = $entity_type->getPluralLabel(); + + if ($has_owner) { + $permissions["view any {$entity_type_id}"] = [ + 'title' => $this->t('View any @type', [ + '@type' => $plural_label, + ]), + ]; + $permissions["view own {$entity_type_id}"] = [ + 'title' => $this->t('View own @type', [ + '@type' => $plural_label, + ]), + ]; + } + else { + $permissions["view {$entity_type_id}"] = [ + 'title' => $this->t('View @type', [ + '@type' => $plural_label, + ]), + ]; + } + + return $permissions; + } + + /** + * Builds permissions for the bundle granularity. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return array + * The permissions. + */ + protected function buildBundlePermissions(EntityTypeInterface $entity_type) { + $permissions = parent::buildBundlePermissions($entity_type); + $entity_type_id = $entity_type->id(); + $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + $has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class); + $plural_label = $entity_type->getPluralLabel(); + + if ($has_owner) { + $permissions["view any {$entity_type_id}"] = [ + 'title' => $this->t('View any @type', [ + '@type' => $plural_label, + ]), + ]; + $permissions["view own {$entity_type_id}"] = [ + 'title' => $this->t('View own @type', [ + '@type' => $plural_label, + ]), + ]; + } + else { + $permissions["view {$entity_type_id}"] = [ + 'title' => $this->t('View @type', [ + '@type' => $plural_label, + ]), + ]; + } + + foreach ($bundles as $bundle_name => $bundle_info) { + if ($has_owner) { + $permissions["view any {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View any @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + $permissions["view own {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View own @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + } + else { + $permissions["view {$bundle_name} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View @type', [ + '@bundle' => $bundle_info['label'], + '@type' => $plural_label, + ]), + ]; + } + } + + return $permissions; + } + +} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml new file mode 100644 index 000000000..0bc69c5c2 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml @@ -0,0 +1,13 @@ +name: 'Entity bundle plugin examples test' +type: module +description: 'Module for testing bundle plugins.' +package: Testing +# core: 8.x +dependencies: + - entity + +# Information added by Drupal.org packaging script on 2018-10-11 +version: '8.x-1.0-rc1' +core: '8.x' +project: 'entity' +datestamp: 1539272605 diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php new file mode 100644 index 000000000..a047b6e81 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php @@ -0,0 +1,31 @@ +setLabel(t('Email')) + ->setRequired(TRUE); + + return $fields; + } + +} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml new file mode 100644 index 000000000..654a84203 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml @@ -0,0 +1,13 @@ +name: 'Entity bundle plugin test' +type: module +description: 'Module for testing bundle plugins.' +package: Testing +# core: 8.x +dependencies: + - entity + +# Information added by Drupal.org packaging script on 2018-10-11 +version: '8.x-1.0-rc1' +core: '8.x' +project: 'entity' +datestamp: 1539272605 diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml new file mode 100644 index 000000000..ead5cd39d --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml @@ -0,0 +1,4 @@ +services: + plugin.manager.bundle_plugin_test: + class: Drupal\entity_module_bundle_plugin_test\BundlePluginTestManager + parent: default_plugin_manager diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php new file mode 100644 index 000000000..f09f70eb2 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php @@ -0,0 +1,34 @@ +alterInfo('bundle_plugin_test_info'); + $this->setCacheBackend($cache_backend, 'bundle_plugin_test_plugins'); + } + +} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php new file mode 100644 index 000000000..1c75a477a --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php @@ -0,0 +1,27 @@ +setLabel(t('Email')) + ->setRequired(TRUE); + + return $fields; + } + +} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.first.assigned.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.first.assigned.yml new file mode 100644 index 000000000..9e9c57738 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.first.assigned.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.entity_test_enhanced.assigned +id: entity_test_enhanced.first.assigned +field_name: assigned +entity_type: entity_test_enhanced +bundle: first +label: Assigned +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.second.assigned.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.second.assigned.yml new file mode 100644 index 000000000..fb259e3f6 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.second.assigned.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.entity_test_enhanced.assigned +id: entity_test_enhanced.second.assigned +field_name: assigned +entity_type: entity_test_enhanced +bundle: second +label: Assigned +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.storage.entity_test_enhanced.assigned.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.storage.entity_test_enhanced.assigned.yml new file mode 100644 index 000000000..3669b5181 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.storage.entity_test_enhanced.assigned.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - entity_module_test +id: entity_test_enhanced.assigned +field_name: assigned +entity_type: entity_test_enhanced +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: true +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced.yml new file mode 100644 index 000000000..ab0ecf500 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced.yml @@ -0,0 +1,170 @@ +langcode: en +status: true +dependencies: + module: + - entity_module_test +id: entity_test_enhanced +label: 'Enhanced entities' +module: views +description: '' +tag: '' +base_table: entity_test_enhanced_field_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: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + name: + table: entity_test_enhanced_field_data + field: name + id: name + entity_type: null + entity_field: name + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: { } + sorts: + id: + id: id + table: entity_test_enhanced_field_data + field: id + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: entity_test_enhanced + entity_field: id + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_revisions.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_revisions.yml new file mode 100644 index 000000000..98def8a6e --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_revisions.yml @@ -0,0 +1,170 @@ +langcode: en +status: true +dependencies: + module: + - entity_module_test +id: entity_test_enhanced_revisions +label: 'Enhanced entities (Revisions)' +module: views +description: '' +tag: '' +base_table: entity_test_enhanced_field_revision +base_field: nid +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: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + name: + table: entity_test_enhanced_field_revision + field: name + id: name + entity_type: null + entity_field: name + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: { } + sorts: + vid: + id: vid + table: entity_test_enhanced_field_revision + field: vid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: entity_test_enhanced + entity_field: vid + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner.yml new file mode 100644 index 000000000..fcee646c2 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner.yml @@ -0,0 +1,170 @@ +langcode: en +status: true +dependencies: + module: + - entity_module_test +id: entity_test_enhanced_with_owner +label: 'Enhanced entities with an owner' +module: views +description: '' +tag: '' +base_table: entity_test_enhanced_with_owner_field_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: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + name: + table: entity_test_enhanced_with_owner_field_data + field: name + id: name + entity_type: null + entity_field: name + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: { } + sorts: + id: + id: id + table: entity_test_enhanced_with_owner_field_data + field: id + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: entity_test_enhanced_with_owner + entity_field: id + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner_revisions.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner_revisions.yml new file mode 100644 index 000000000..6e3c7e933 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner_revisions.yml @@ -0,0 +1,170 @@ +langcode: en +status: true +dependencies: + module: + - entity_module_test +id: entity_test_enhanced_with_owner_revisions +label: 'Enhanced entities with an owner (Revisions)' +module: views +description: '' +tag: '' +base_table: entity_test_enhanced_with_owner_field_revision +base_field: nid +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: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + name: + table: entity_test_enhanced_with_owner_field_revision + field: name + id: name + entity_type: null + entity_field: name + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: { } + sorts: + vid: + id: vid + table: entity_test_enhanced_with_owner_field_revision + field: vid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: entity_test_enhanced_with_owner + entity_field: vid + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml new file mode 100644 index 000000000..3446a3e0b --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml @@ -0,0 +1,12 @@ +name: Entity test +type: module +package: Testing +# core: 8.x +dependencies: + - field + +# Information added by Drupal.org packaging script on 2018-10-11 +version: '8.x-1.0-rc1' +core: '8.x' +project: 'entity' +datestamp: 1539272605 diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml new file mode 100644 index 000000000..e306c10c9 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml @@ -0,0 +1,9 @@ +entity.entity_test_enhanced.canonical: + title: View + route_name: entity.entity_test_enhanced.canonical + base_route: entity.entity_test_enhanced.canonical + +entity.entity_module_test.edit_form: + title: Edit + route_name: entity.entity_test_enhanced.edit_form + base_route: entity.entity_test_enhanced.canonical diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.module b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.module new file mode 100644 index 000000000..bcd14929a --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.module @@ -0,0 +1,15 @@ +setLabel('Name') + ->setRevisionable(TRUE) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'string', + 'weight' => -5, + ]); + + return $fields; + } + +} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityWithOwner.php b/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityWithOwner.php new file mode 100644 index 000000000..214fb21bf --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityWithOwner.php @@ -0,0 +1,145 @@ +get('user_id')->entity; + } + + /** + * {@inheritdoc} + */ + public function getOwnerId() { + return $this->get('user_id')->target_id; + } + + /** + * {@inheritdoc} + */ + public function setOwnerId($uid) { + $this->set('user_id', $uid); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOwner(UserInterface $account) { + $this->set('user_id', $account->id()); + return $this; + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + $fields += static::publishedBaseFieldDefinitions($entity_type); + + $fields['name'] = BaseFieldDefinition::create('string') + ->setLabel('Name') + ->setRevisionable(TRUE) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'string', + 'weight' => -5, + ]); + + $fields['user_id'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('User ID')) + ->setDescription(t('The ID of the associated user.')) + ->setSetting('target_type', 'user') + ->setSetting('handler', 'default') + // Default EntityTest entities to have the root user as the owner, to + // simplify testing. + ->setDefaultValue([0 => ['target_id' => 1]]) + ->setTranslatable(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'weight' => -1, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'placeholder' => '', + ], + ]); + + return $fields; + } + +} diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php b/web/modules/contrib/entity/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php new file mode 100644 index 000000000..5f7ffb607 --- /dev/null +++ b/web/modules/contrib/entity/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php @@ -0,0 +1,58 @@ + 'onQueryAccess', + ]; + } + + /** + * Modifies the access conditions based on the current user. + * + * This is just a convenient example for testing. A real subscriber would + * ignore the account and extend the conditions to cover additional factors, + * such as a custom entity field. + * + * @param \Drupal\entity\QueryAccess\QueryAccessEvent $event + * The event. + */ + public function onQueryAccess(QueryAccessEvent $event) { + $conditions = $event->getConditions(); + $email = $event->getAccount()->getEmail(); + + if ($email == 'user1@example.com') { + // This user should not have access to any entities. + $conditions->alwaysFalse(); + } + elseif ($email == 'user2@example.com') { + // This user should have access to entities with the IDs 1, 2, and 3. + // The query access handler might have already set ->alwaysFalse() + // due to the user not having any other access, so we make sure + // to undo it with $conditions->alwaysFalse(TRUE). + $conditions->alwaysFalse(FALSE); + $conditions->addCondition('id', ['1', '2', '3']); + } + elseif ($email == 'user3@example.com') { + // This user should only have access to entities assigned to "marketing", + // or unassigned entities. + $conditions->alwaysFalse(FALSE); + $conditions->addCondition((new ConditionGroup('OR')) + ->addCondition('assigned', NULL, 'IS NULL') + // Confirm that explicitly specifying the property name works. + ->addCondition('assigned.value', 'marketing') + ); + } + } + +} diff --git a/web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php b/web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php new file mode 100644 index 000000000..2e7772b2a --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php @@ -0,0 +1,71 @@ +placeBlock('local_tasks_block'); + $this->placeBlock('system_breadcrumb_block'); + } + + /** + * Test the collection route access. + */ + public function testCollectionRouteAccess() { + $entity = EnhancedEntity::create([ + 'name' => 'rev 1', + 'type' => 'default', + ]); + $entity->save(); + + // User without any relevant permissions. + $account = $this->drupalCreateUser(['access administration pages']); + $this->drupalLogin($account); + + $this->drupalGet($entity->toUrl('collection')); + $this->assertSession()->statusCodeEquals(403); + + // User with "access overview" permissions. + $account = $this->drupalCreateUser(['access entity_test_enhanced overview']); + $this->drupalLogin($account); + + $this->drupalGet($entity->toUrl('collection')); + $this->assertSession()->statusCodeEquals(200); + + // User with full administration permissions. + $account = $this->drupalCreateUser(['administer entity_test_enhanced']); + $this->drupalLogin($account); + + $this->drupalGet($entity->toUrl('collection')); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php b/web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php new file mode 100644 index 000000000..aaf70c60e --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php @@ -0,0 +1,76 @@ +account = $this->drupalCreateUser(['administer entity_test_enhanced']); + $this->drupalLogin($this->account); + } + + /** + * Tests the add page. + */ + public function testForm() { + $entities = []; + $selection = []; + for ($i = 0; $i < 2; $i++) { + $entity = EnhancedEntity::create([ + 'type' => 'default', + ]); + $entity->save(); + $entities[$entity->id()] = $entity; + + $langcode = $entity->language()->getId(); + $selection[$entity->id()][$langcode] = $langcode; + } + // Add the selection to the tempstore just like DeleteAction would. + $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm'); + $tempstore->set($this->account->id() . ':entity_test_enhanced', $selection); + + $this->drupalGet('/entity_test_enhanced/delete'); + $assert = $this->assertSession(); + $assert->statusCodeEquals(200); + $assert->elementTextContains('css', '.page-title', 'Are you sure you want to delete these enhanced entities?'); + $delete_button = $this->getSession()->getPage()->findButton('Delete'); + $delete_button->click(); + $assert = $this->assertSession(); + $assert->addressEquals('/entity_test_enhanced'); + $assert->responseContains('Deleted 2 items.'); + + \Drupal::entityTypeManager()->getStorage('entity_test_enhanced')->resetCache(); + $remaining_entities = EnhancedEntity::loadMultiple(array_keys($selection)); + $this->assertEmpty($remaining_entities); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php b/web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php new file mode 100644 index 000000000..ca4e1192e --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php @@ -0,0 +1,44 @@ +drupalPlaceBlock('local_actions_block'); + + $account = $this->drupalCreateUser(['administer entity_test_enhanced']); + $this->drupalLogin($account); + } + + /** + * Tests the local action on the collection is provided correctly. + */ + public function testCollectionLocalAction() { + $this->drupalGet('/entity_test_enhanced'); + $this->assertSession()->linkByHrefExists('/entity_test_enhanced/add?destination=/entity_test_enhanced'); + $this->assertSession()->linkExists('Add enhanced entity'); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php b/web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php new file mode 100644 index 000000000..c40a8303b --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php @@ -0,0 +1,91 @@ +placeBlock('local_tasks_block'); + $this->placeBlock('system_breadcrumb_block'); + + $this->account = $this->drupalCreateUser([ + 'administer entity_test_enhanced', + 'view all entity_test_enhanced revisions', + ]); + + $this->drupalLogin($this->account); + } + + /** + * Test enhanced entity revision routes access. + */ + public function testRevisionRouteAccess() { + $entity = EnhancedEntity::create([ + 'name' => 'rev 1', + 'type' => 'default', + ]); + $entity->save(); + + $revision = clone $entity; + $revision->name->value = 'rev 2'; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(FALSE); + $revision->save(); + + $this->drupalGet('/entity_test_enhanced/1/revisions'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->responseContains('Revisions'); + $collection_link = $this->getSession()->getPage()->findLink('Enhanced entities'); + $collection_link->click(); + $this->assertSession()->addressEquals('/entity_test_enhanced'); + $this->assertSession()->responseContains('Edit'); + $edit_link = $this->getSession()->getPage()->findLink('Edit'); + $edit_link->click(); + $this->assertSession()->addressEquals('/entity_test_enhanced/1/edit'); + // Check if we have revision tab link on edit page. + $this->getSession()->getPage()->findLink('Revisions')->click(); + $this->assertSession()->addressEquals('/entity_test_enhanced/1/revisions'); + $this->drupalGet('/entity_test_enhanced/1/revisions/2/view'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->responseContains('rev 2'); + $revisions_link = $this->getSession()->getPage()->findLink('Revisions'); + $revisions_link->click(); + $this->assertSession()->addressEquals('/entity_test_enhanced/1/revisions'); + $this->assertSession()->statusCodeEquals(200); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php b/web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php new file mode 100644 index 000000000..eeeb92384 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php @@ -0,0 +1,146 @@ +installSchema('system', 'router'); + + // Install the modules properly. Putting them into static::$modules doesn't trigger the install + // hooks, like hook_modules_installed, so entity_modules_installed is not triggered(). + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */ + $module_installer = \Drupal::service('module_installer'); + $module_installer->install(['entity_module_bundle_plugin_test', 'entity_module_bundle_plugin_examples_test']); + } + + /** + * Tests the bundle plugins. + */ + public function testPluginBundles() { + $bundled_entity_types = entity_get_bundle_plugin_entity_types(); + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + $entity_type = $bundled_entity_types['entity_test_bundle_plugin']; + $this->assertEquals('entity_test_bundle_plugin', $entity_type->id()); + $this->assertTrue($entity_type->hasHandlerClass('bundle_plugin')); + + /** @var \Drupal\Core\Entity\EntityTypeBundleInfo $entity_type_bundle_info */ + $entity_type_bundle_info = \Drupal::service('entity_type.bundle.info'); + $bundle_info = $entity_type_bundle_info->getBundleInfo('entity_test_bundle_plugin'); + $this->assertEquals(2, count($bundle_info)); + $this->assertArrayHasKey('first', $bundle_info); + $this->assertArrayHasKey('second', $bundle_info); + $this->assertEquals('First', $bundle_info['first']['label']); + $this->assertEquals('Some description', $bundle_info['first']['description']); + + /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ + $entity_field_manager = \Drupal::service('entity_field.manager'); + $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions('entity_test_bundle_plugin'); + $this->assertArrayHasKey('first_mail', $field_storage_definitions); + $this->assertArrayHasKey('second_mail', $field_storage_definitions); + $first_field_definitions = $entity_field_manager->getFieldDefinitions('entity_test_bundle_plugin', 'first'); + $this->assertArrayHasKey('first_mail', $first_field_definitions); + $this->assertArrayNotHasKey('second_mail', $first_field_definitions); + $second_field_definitions = $entity_field_manager->getFieldDefinitions('entity_test_bundle_plugin', 'second'); + $this->assertArrayNotHasKey('first_mail', $second_field_definitions); + $this->assertArrayHasKey('second_mail', $second_field_definitions); + + $first_entity = EntityTestBundlePlugin::create([ + 'type' => 'first', + 'first_mail' => 'admin@test.com', + ]); + $first_entity->save(); + $first_entity = EntityTestBundlePlugin::load($first_entity->id()); + $this->assertEquals('admin@test.com', $first_entity->first_mail->value); + + $second_entity = EntityTestBundlePlugin::create([ + 'type' => 'second', + 'second_mail' => 'admin@example.com', + ]); + $second_entity->save(); + $second_entity = EntityTestBundlePlugin::load($second_entity->id()); + $this->assertEquals('admin@example.com', $second_entity->second_mail->value); + + // Also test entity queries. + $result = \Drupal::entityTypeManager()->getStorage('entity_test_bundle_plugin') + ->getQuery() + ->condition('second_mail', 'admin@example.com') + ->execute(); + $this->assertEquals([$second_entity->id() => $second_entity->id()], $result); + + $result = \Drupal::entityTypeManager()->getStorage('entity_test_bundle_plugin') + ->getQuery() + ->condition('type', 'first') + ->execute(); + $this->assertEquals([$first_entity->id() => $first_entity->id()], $result); + + } + + /** + * Tests the uninstallation for a bundle provided by a module. + */ + public function testBundlePluginModuleUninstallation() { + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */ + $module_installer = \Drupal::service('module_installer'); + + // One should be possible to uninstall without any actual content. + $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']); + $this->assertEmpty($violations); + + $first_entity = EntityTestBundlePlugin::create([ + 'type' => 'first', + 'first_mail' => 'admin@test.com', + ]); + $first_entity->save(); + $second_entity = EntityTestBundlePlugin::create([ + 'type' => 'second', + 'second_mail' => 'admin@example.com', + ]); + $second_entity->save(); + + $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']); + $this->assertCount(1, $violations); + $this->assertCount(1, $violations['entity_module_bundle_plugin_examples_test']); + $this->assertEquals('There is data for the bundle Second on the entity type Entity test bundle plugin. Please remove all content before uninstalling the module.', $violations['entity_module_bundle_plugin_examples_test'][0]); + + $second_entity->delete(); + + // The first entity is defined by entity_module_bundle_plugin_test, so it should be possible + // to uninstall the module providing the second bundle plugin. + $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']); + $this->assertEmpty($violations); + + $module_installer->uninstall(['entity_module_bundle_plugin_examples_test']); + + // The first entity is provided by entity_module_bundle_plugin_test which we cannot uninstall, + // until all the entities are deleted. + $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_test']); + $this->assertNotEmpty($violations); + + $first_entity->delete(); + $violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_test']); + $this->assertEmpty($violations); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php b/web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php new file mode 100644 index 000000000..1f2f35a0c --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php @@ -0,0 +1,78 @@ +installEntitySchema('user'); + $this->installEntitySchema('entity_test_enhanced'); + $this->installSchema('system', ['key_value_expire', 'sequences']); + + $this->user = User::create([ + 'name' => 'username', + 'status' => 1, + ]); + $this->user->save(); + \Drupal::service('current_user')->setAccount($this->user); + } + + public function testAction() { + /** @var \Drupal\system\ActionConfigEntityInterface $action */ + $action = Action::create([ + 'id' => 'enhanced_entity_delete_action', + 'label' => 'Delete enhanced entity', + 'plugin' => 'entity_delete_action:entity_test_enhanced', + ]); + $status = $action->save(); + $this->assertEquals(SAVED_NEW, $status); + $this->assertInstanceOf(DeleteAction::class, $action->getPlugin()); + + $entities = []; + for ($i = 0; $i < 2; $i++) { + $entity = EnhancedEntity::create([ + 'type' => 'default', + ]); + $entity->save(); + $entities[$entity->id()] = $entity; + } + + $action->execute($entities); + // Confirm that the entity ids and langcodes are now in the tempstore. + $tempstore = \Drupal::service('tempstore.private')->get('entity_delete_multiple_confirm'); + $selection = $tempstore->get($this->user->id() . ':entity_test_enhanced'); + $this->assertEquals(array_keys($entities), array_keys($selection)); + $this->assertEquals([['en' => 'en'], ['en' => 'en']], array_values($selection)); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/ConditionGroupTest.php b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/ConditionGroupTest.php new file mode 100644 index 000000000..34dc09cea --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/ConditionGroupTest.php @@ -0,0 +1,106 @@ +addCondition('uid', '2'); + $this->assertEquals('AND', $condition_group->getConjunction()); + $expected_conditions = [ + new Condition('uid', '2'), + ]; + $this->assertEquals($expected_conditions, $condition_group->getConditions()); + $this->assertEquals(1, $condition_group->count()); + $this->assertEquals("uid = '2'", $condition_group->__toString()); + + $condition_group = new ConditionGroup('OR'); + $condition_group->addCondition('type', ['article', 'page']); + $condition_group->addCondition('status', '1', '<>'); + $this->assertEquals('OR', $condition_group->getConjunction()); + $expected_conditions = [ + new Condition('type', ['article', 'page']), + new Condition('status', '1', '<>'), + ]; + $expected_lines = [ + "(", + " type IN ['article', 'page']", + " OR", + " status <> '1'", + ")", + ]; + $this->assertEquals($expected_conditions, $condition_group->getConditions()); + $this->assertEquals(2, $condition_group->count()); + $this->assertEquals(implode("\n", $expected_lines), $condition_group->__toString()); + + // Nested condition group with a single condition. + $condition_group = new ConditionGroup(); + $condition_group->addCondition('type', ['article', 'page']); + $condition_group->addCondition((new ConditionGroup('AND')) + ->addCondition('status', '1') + ); + $expected_conditions = [ + new Condition('type', ['article', 'page']), + new Condition('status', '1'), + ]; + $expected_lines = [ + "(", + " type IN ['article', 'page']", + " AND", + " status = '1'", + ")", + ]; + $this->assertEquals($expected_conditions, $condition_group->getConditions()); + $this->assertEquals('AND', $condition_group->getConjunction()); + $this->assertEquals(2, $condition_group->count()); + $this->assertEquals(implode("\n", $expected_lines), $condition_group->__toString()); + + // Nested condition group with multiple conditions. + $condition_group = new ConditionGroup(); + $condition_group->addCondition('type', ['article', 'page']); + $nested_condition_group = new ConditionGroup('OR'); + $nested_condition_group->addCondition('uid', '1'); + $nested_condition_group->addCondition('status', '1'); + $condition_group->addCondition($nested_condition_group); + $expected_conditions = [ + new Condition('type', ['article', 'page']), + $nested_condition_group, + ]; + $expected_lines = [ + "(", + " type IN ['article', 'page']", + " AND", + " (", + " uid = '1'", + " OR", + " status = '1'", + " )", + ")", + ]; + $this->assertEquals($expected_conditions, $condition_group->getConditions()); + $this->assertEquals('AND', $condition_group->getConjunction()); + $this->assertEquals(2, $condition_group->count()); + $this->assertEquals(implode("\n", $expected_lines), $condition_group->__toString()); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessEventTest.php b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessEventTest.php new file mode 100644 index 000000000..8ecef3b17 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessEventTest.php @@ -0,0 +1,62 @@ +installEntitySchema('entity_test_enhanced'); + + // Create uid: 1 here so that it's skipped in test cases. + $admin_user = $this->createUser(); + + $entity_type_manager = $this->container->get('entity_type.manager'); + $entity_type = $entity_type_manager->getDefinition('entity_test_enhanced'); + $this->handler = QueryAccessHandler::createInstance($this->container, $entity_type); + } + + /** + * Tests the event. + */ + public function testEvent() { + // By default, the first user should have full access, and the second + // user should have no access. The QueryAccessSubscriber flips that. + $first_user = $this->createUser(['mail' => 'user1@example.com'], ['administer entity_test_enhanced']); + $second_user = $this->createUser(['mail' => 'user2@example.com']); + + $conditions = $this->handler->getConditions('view', $first_user); + $this->assertTrue($conditions->isAlwaysFalse()); + + $conditions = $this->handler->getConditions('view', $second_user); + $this->assertFalse($conditions->isAlwaysFalse()); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessHandlerTest.php b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessHandlerTest.php new file mode 100644 index 000000000..6ac3e6c15 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessHandlerTest.php @@ -0,0 +1,137 @@ +installEntitySchema('entity_test_enhanced'); + + // Create uid: 1 here so that it's skipped in test cases. + $admin_user = $this->createUser(); + + $entity_type_manager = $this->container->get('entity_type.manager'); + $entity_type = $entity_type_manager->getDefinition('entity_test_enhanced'); + $this->handler = QueryAccessHandler::createInstance($this->container, $entity_type); + } + + /** + * @covers ::getConditions + */ + public function testNoAccess() { + foreach (['view', 'update', 'delete'] as $operation) { + $user = $this->createUser([], ['access content']); + $conditions = $this->handler->getConditions($operation, $user); + $this->assertEquals(0, $conditions->count()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertTrue($conditions->isAlwaysFalse()); + } + } + + /** + * @covers ::getConditions + */ + public function testAdmin() { + foreach (['view', 'update', 'delete'] as $operation) { + $user = $this->createUser([], ['administer entity_test_enhanced']); + $conditions = $this->handler->getConditions($operation, $user); + $this->assertEquals(0, $conditions->count()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + } + } + + /** + * @covers ::getConditions + */ + public function testView() { + // Entity type permission. + $user = $this->createUser([], ['view entity_test_enhanced']); + $conditions = $this->handler->getConditions('view', $user); + $expected_conditions = [ + new Condition('status', '1'), + ]; + $this->assertEquals(1, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // Bundle permission. + $user = $this->createUser([], ['view first entity_test_enhanced']); + $conditions = $this->handler->getConditions('view', $user); + $expected_conditions = [ + new Condition('type', ['first']), + new Condition('status', '1'), + ]; + $this->assertEquals('AND', $conditions->getConjunction()); + $this->assertEquals(2, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + } + + /** + * @covers ::getConditions + */ + public function testUpdateDelete() { + foreach (['update', 'delete'] as $operation) { + // Entity type permission. + $user = $this->createUser([], ["$operation entity_test_enhanced"]); + $conditions = $this->handler->getConditions($operation, $user); + $this->assertEquals(0, $conditions->count()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // Bundle permission. + $user = $this->createUser([], [ + "$operation first entity_test_enhanced", + "$operation second entity_test_enhanced", + ]); + $conditions = $this->handler->getConditions($operation, $user); + $expected_conditions = [ + new Condition('type', ['first', 'second']), + ]; + $this->assertEquals('OR', $conditions->getConjunction()); + $this->assertEquals(1, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + } + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessTest.php b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessTest.php new file mode 100644 index 000000000..068e62af5 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessTest.php @@ -0,0 +1,343 @@ +installEntitySchema('entity_test_enhanced'); + $this->installConfig(['entity_module_test']); + + // Create uid: 1 here so that it's skipped in test cases. + $admin_user = $this->createUser(); + + $first_entity = EnhancedEntity::create([ + 'type' => 'first', + 'label' => 'First', + 'status' => 1, + ]); + $first_entity->save(); + + $first_entity->set('name', 'First!'); + $first_entity->set('status', 0); + $first_entity->setNewRevision(TRUE); + $first_entity->save(); + + $second_entity = EnhancedEntity::create([ + 'type' => 'first', + 'label' => 'Second', + 'status' => 0, + ]); + $second_entity->save(); + + $second_entity->set('name', 'Second!'); + $second_entity->set('status', 1); + $second_entity->setNewRevision(TRUE); + $second_entity->save(); + + $third_entity = EnhancedEntity::create([ + 'type' => 'second', + 'label' => 'Third', + 'status' => 1, + ]); + $third_entity->save(); + + $third_entity->set('name', 'Third!'); + $third_entity->setNewRevision(TRUE); + $third_entity->save(); + + $this->entities = [$first_entity, $second_entity, $third_entity]; + $this->storage = \Drupal::entityTypeManager()->getStorage('entity_test_enhanced'); + } + + /** + * Tests EntityQuery filtering. + */ + public function testEntityQuery() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced']); + \Drupal::currentUser()->setAccount($admin_user); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[0]->id(), + $this->entities[1]->id(), + $this->entities[2]->id(), + ], array_values($result)); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->execute(); + $this->assertEmpty($result); + + // View (published-only). + $user = $this->createUser([], ['view entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[1]->id(), + $this->entities[2]->id(), + ], array_values($result)); + + // View $bundle (published-only). + $user = $this->createUser([], ['view first entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[1]->id(), + ], array_values($result)); + } + + /** + * Tests EntityQuery filtering when all revisions are queried. + */ + public function testEntityQueryWithRevisions() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced']); + \Drupal::currentUser()->setAccount($admin_user); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '2' => $this->entities[0]->id(), + '3' => $this->entities[1]->id(), + '4' => $this->entities[1]->id(), + '5' => $this->entities[2]->id(), + '6' => $this->entities[2]->id(), + ], $result); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->execute(); + $this->assertEmpty($result); + + // View (published-only). + $user = $this->createUser([], ['view entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '4' => $this->entities[1]->id(), + '5' => $this->entities[2]->id(), + '6' => $this->entities[2]->id(), + ], $result); + + // View $bundle (published-only). + $user = $this->createUser([], ['view first entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '4' => $this->entities[1]->id(), + ], $result); + } + + /** + * Tests Views filtering. + */ + public function testViews() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced']); + \Drupal::currentUser()->setAccount($admin_user); + + $view = Views::getView('entity_test_enhanced'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[0]->id()], + ['id' => $this->entities[1]->id()], + ['id' => $this->entities[2]->id()], + ], ['id' => 'id']); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced'); + $view->execute(); + $this->assertIdenticalResultset($view, []); + + // View (published-only). + $user = $this->createUser([], ['view entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[1]->id()], + ['id' => $this->entities[2]->id()], + ], ['id' => 'id']); + + // View $bundle (published-only). + $user = $this->createUser([], ['view first entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[1]->id()], + ], ['id' => 'id']); + } + + /** + * Tests Views filtering when all revisions are queried. + */ + public function testViewsWithRevisions() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced']); + \Drupal::currentUser()->setAccount($admin_user); + + $view = Views::getView('entity_test_enhanced_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '2', 'id' => $this->entities[0]->id()], + ['vid' => '3', 'id' => $this->entities[1]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ['vid' => '5', 'id' => $this->entities[2]->id()], + ['vid' => '6', 'id' => $this->entities[2]->id()], + ], ['vid' => 'vid']); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced'); + $view->execute(); + $this->assertIdenticalResultset($view, []); + + // View (published-only). + $user = $this->createUser([], ['view entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ['vid' => '5', 'id' => $this->entities[2]->id()], + ['vid' => '6', 'id' => $this->entities[2]->id()], + ], ['vid' => 'vid']); + + // View $bundle (published-only). + $user = $this->createUser([], ['view first entity_test_enhanced']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ], ['vid' => 'vid']); + } + + /** + * Tests filtering based on a configurable field. + * + * QueryAccessSubscriber adds a condition that ensures that the field value + * is either empty or matches "marketing". + * + * @see \Drupal\entity_module_test\EventSubscriber\QueryAccessSubscriber + */ + public function testConfigurableField() { + $this->entities[0]->set('assigned', 'marketing'); + $this->entities[0]->save(); + // The field is case sensitive, so the third entity should be ignored. + $this->entities[2]->set('assigned', 'MarKeTing'); + $this->entities[2]->save(); + $user = $this->createUser([ + 'mail' => 'user3@example.com', + ], ['access content']); + \Drupal::currentUser()->setAccount($user); + + // EntityQuery. + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[0]->id(), + $this->entities[1]->id(), + ], array_values($result)); + + // EntityQuery with revisions. + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '2' => $this->entities[0]->id(), + '3' => $this->entities[1]->id(), + '4' => $this->entities[1]->id(), + '5' => $this->entities[2]->id(), + ], $result); + + // View. + $view = Views::getView('entity_test_enhanced'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[0]->id()], + ['id' => $this->entities[1]->id()], + ], ['id' => 'id']); + + // View with revisions. + $view = Views::getView('entity_test_enhanced_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '2', 'id' => $this->entities[0]->id()], + ['vid' => '3', 'id' => $this->entities[1]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ['vid' => '5', 'id' => $this->entities[2]->id()], + ], ['vid' => 'vid']); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessHandlerTest.php b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessHandlerTest.php new file mode 100644 index 000000000..761a1f7a6 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessHandlerTest.php @@ -0,0 +1,203 @@ +installEntitySchema('entity_test_enhanced_with_owner'); + + // Create uid: 1 here so that it's skipped in test cases. + $admin_user = $this->createUser(); + + $entity_type_manager = $this->container->get('entity_type.manager'); + $entity_type = $entity_type_manager->getDefinition('entity_test_enhanced_with_owner'); + $this->handler = UncacheableQueryAccessHandler::createInstance($this->container, $entity_type); + } + + /** + * @covers ::getConditions + */ + public function testNoAccess() { + foreach (['view', 'update', 'delete'] as $operation) { + $user = $this->createUser([], ['access content']); + $conditions = $this->handler->getConditions($operation, $user); + $this->assertEquals(0, $conditions->count()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertTrue($conditions->isAlwaysFalse()); + } + } + + /** + * @covers ::getConditions + */ + public function testAdmin() { + foreach (['view', 'update', 'delete'] as $operation) { + $user = $this->createUser([], ['administer entity_test_enhanced_with_owner']); + $conditions = $this->handler->getConditions($operation, $user); + $this->assertEquals(0, $conditions->count()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + } + } + + /** + * @covers ::getConditions + */ + public function testView() { + // Any permission. + $user = $this->createUser([], ['view any entity_test_enhanced_with_owner']); + $conditions = $this->handler->getConditions('view', $user); + $expected_conditions = [ + new Condition('status', '1'), + ]; + $this->assertEquals(1, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // Own permission. + $user = $this->createUser([], ['view own entity_test_enhanced_with_owner']); + $conditions = $this->handler->getConditions('view', $user); + $expected_conditions = [ + new Condition('user_id', $user->id()), + new Condition('status', '1'), + ]; + $this->assertEquals('AND', $conditions->getConjunction()); + $this->assertEquals(2, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // Any permission for the first bundle, own permission for the second. + $user = $this->createUser([], [ + 'view any first entity_test_enhanced_with_owner', + 'view own second entity_test_enhanced_with_owner', + ]); + $conditions = $this->handler->getConditions('view', $user); + $expected_conditions = [ + (new ConditionGroup('OR')) + ->addCacheContexts(['user', 'user.permissions']) + ->addCondition('type', ['first']) + ->addCondition((new ConditionGroup('AND')) + ->addCondition('user_id', $user->id()) + ->addCondition('type', ['second']) + ), + new Condition('status', '1'), + ]; + $this->assertEquals('AND', $conditions->getConjunction()); + $this->assertEquals(2, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // View own unpublished permission. + $user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']); + $conditions = $this->handler->buildConditions('view', $user); + $expected_conditions = [ + new Condition('user_id', $user->id()), + new Condition('status', '0'), + ]; + $this->assertEquals(2, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // Both view any and view own unpublished permissions. + $user = $this->createUser([], [ + 'view any entity_test_enhanced_with_owner', + 'view own unpublished entity_test_enhanced_with_owner', + ]); + $conditions = $this->handler->buildConditions('view', $user); + $expected_conditions = [ + new Condition('status', '1'), + (new ConditionGroup('AND')) + ->addCondition('user_id', $user->id()) + ->addCondition('status', '0') + ->addCacheContexts(['user']), + ]; + $this->assertEquals(2, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + } + + /** + * @covers ::getConditions + */ + public function testUpdateDelete() { + foreach (['update', 'delete'] as $operation) { + // Any permission. + $user = $this->createUser([], ["$operation any entity_test_enhanced_with_owner"]); + $conditions = $this->handler->getConditions($operation, $user); + $this->assertEquals(0, $conditions->count()); + $this->assertEquals(['user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // Own permission. + $user = $this->createUser([], ["$operation own entity_test_enhanced_with_owner"]); + $conditions = $this->handler->getConditions($operation, $user); + $expected_conditions = [ + new Condition('user_id', $user->id()), + ]; + $this->assertEquals(1, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + + // Any permission for the first bundle, own permission for the second. + $user = $this->createUser([], [ + "$operation any first entity_test_enhanced_with_owner", + "$operation own second entity_test_enhanced_with_owner", + ]); + $conditions = $this->handler->getConditions($operation, $user); + $expected_conditions = [ + new Condition('type', ['first']), + (new ConditionGroup('AND')) + ->addCondition('user_id', $user->id()) + ->addCondition('type', ['second']), + ]; + $this->assertEquals('OR', $conditions->getConjunction()); + $this->assertEquals(2, $conditions->count()); + $this->assertEquals($expected_conditions, $conditions->getConditions()); + $this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts()); + $this->assertFalse($conditions->isAlwaysFalse()); + } + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessTest.php b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessTest.php new file mode 100644 index 000000000..bcd0d3585 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessTest.php @@ -0,0 +1,511 @@ +installEntitySchema('entity_test_enhanced_with_owner'); + $this->installConfig(['entity_module_test']); + + // Create uid: 1 here so that it's skipped in test cases. + $admin_user = $this->createUser(); + + $first_entity = EnhancedEntityWithOwner::create([ + 'type' => 'first', + 'name' => 'First', + 'status' => 1, + ]); + $first_entity->save(); + + $first_entity->set('name', 'First!'); + $first_entity->set('status', 0); + $first_entity->setNewRevision(TRUE); + $first_entity->save(); + + $second_entity = EnhancedEntityWithOwner::create([ + 'type' => 'first', + 'name' => 'Second', + 'status' => 0, + ]); + $second_entity->save(); + + $second_entity->set('name', 'Second!'); + $second_entity->set('status', 1); + $second_entity->setNewRevision(TRUE); + $second_entity->save(); + + $third_entity = EnhancedEntityWithOwner::create([ + 'type' => 'second', + 'name' => 'Third', + 'status' => 1, + ]); + $third_entity->save(); + + $third_entity->set('name', 'Third!'); + $third_entity->setNewRevision(TRUE); + $third_entity->save(); + + $this->entities = [$first_entity, $second_entity, $third_entity]; + $this->storage = \Drupal::entityTypeManager()->getStorage('entity_test_enhanced_with_owner'); + } + + /** + * Tests EntityQuery filtering. + */ + public function testEntityQuery() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($admin_user); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[0]->id(), + $this->entities[1]->id(), + $this->entities[2]->id(), + ], array_values($result)); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->execute(); + $this->assertEmpty($result); + + // View own (published-only). + $user = $this->createUser([], ['view own entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[1]->id(), + ], array_values($result)); + + // View any (published-only). + $user = $this->createUser([], ['view any entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[1]->id(), + $this->entities[2]->id(), + ], array_values($result)); + + // View own unpublished. + $user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[0]->id(), + ], array_values($result)); + + // View own unpublished + view any (published-only). + $user = $this->createUser([], [ + 'view own unpublished entity_test_enhanced_with_owner', + 'view any entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[0]->id(), + $this->entities[1]->id(), + $this->entities[2]->id(), + ], array_values($result)); + + // View own $first_bundle + View any $second_bundle. + $user = $this->createUser([], [ + 'view own first entity_test_enhanced_with_owner', + 'view any second entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $result = $this->storage->getQuery()->sort('id')->execute(); + $this->assertEquals([ + $this->entities[1]->id(), + $this->entities[2]->id(), + ], array_values($result)); + } + + /** + * Tests EntityQuery filtering when all revisions are queried. + */ + public function testEntityQueryWithRevisions() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($admin_user); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '2' => $this->entities[0]->id(), + '3' => $this->entities[1]->id(), + '4' => $this->entities[1]->id(), + '5' => $this->entities[2]->id(), + '6' => $this->entities[2]->id(), + ], $result); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->allRevisions()->execute(); + $this->assertEmpty($result); + + // View own (published-only). + $user = $this->createUser([], ['view own entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + // The user_id field is not revisionable, which means that updating it + // will modify both revisions for each entity. + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '4' => $this->entities[1]->id(), + ], $result); + + // View any (published-only). + $user = $this->createUser([], ['view any entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '4' => $this->entities[1]->id(), + '5' => $this->entities[2]->id(), + '6' => $this->entities[2]->id(), + ], $result); + + // View own unpublished. + $user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '2' => $this->entities[0]->id(), + '3' => $this->entities[1]->id(), + ], $result); + + // View own unpublished + view any (published-only). + $user = $this->createUser([], [ + 'view own unpublished entity_test_enhanced_with_owner', + 'view any entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '1' => $this->entities[0]->id(), + '2' => $this->entities[0]->id(), + '4' => $this->entities[1]->id(), + '5' => $this->entities[2]->id(), + '6' => $this->entities[2]->id(), + ], $result); + + // View own $first_bundle + View any $second_bundle. + $user = $this->createUser([], [ + 'view own first entity_test_enhanced_with_owner', + 'view any second entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $result = $this->storage->getQuery()->allRevisions()->sort('id')->execute(); + $this->assertEquals([ + '4' => $this->entities[1]->id(), + '5' => $this->entities[2]->id(), + '6' => $this->entities[2]->id(), + ], $result); + } + + /** + * Tests Views filtering. + */ + public function testViews() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($admin_user); + + $view = Views::getView('entity_test_enhanced_with_owner'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[0]->id()], + ['id' => $this->entities[1]->id()], + ['id' => $this->entities[2]->id()], + ], ['id' => 'id']); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced_with_owner'); + $view->execute(); + $this->assertIdenticalResultset($view, []); + + // View own (published-only). + $user = $this->createUser([], ['view own entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[1]->id()], + ], ['id' => 'id']); + + // View any (published-only). + $user = $this->createUser([], ['view any entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced_with_owner'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[1]->id()], + ['id' => $this->entities[2]->id()], + ], ['id' => 'id']); + + // View own unpublished. + $user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[0]->id()], + ], ['id' => 'id']); + + // View own unpublished + view any (published-only). + $user = $this->createUser([], [ + 'view own unpublished entity_test_enhanced_with_owner', + 'view any entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[0]->id()], + ['id' => $this->entities[1]->id()], + ['id' => $this->entities[2]->id()], + ], ['id' => 'id']); + + // View own $first_bundle + View any $second_bundle. + $user = $this->createUser([], [ + 'view own first entity_test_enhanced_with_owner', + 'view any second entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['id' => $this->entities[1]->id()], + ['id' => $this->entities[2]->id()], + ], ['id' => 'id']); + } + + /** + * Tests Views filtering when all revisions are queried. + */ + public function testViewsWithRevisions() { + // Admin permission, full access. + $admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($admin_user); + + $view = Views::getView('entity_test_enhanced_with_owner_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '2', 'id' => $this->entities[0]->id()], + ['vid' => '3', 'id' => $this->entities[1]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ['vid' => '5', 'id' => $this->entities[2]->id()], + ['vid' => '6', 'id' => $this->entities[2]->id()], + ], ['vid' => 'vid']); + + // No view permissions, no access. + $user = $this->createUser([], ['access content']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced_with_owner_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, []); + + // View own (published-only). + $user = $this->createUser([], ['view own entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ], ['vid' => 'vid']); + + // View any (published-only). + $user = $this->createUser([], ['view any entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $view = Views::getView('entity_test_enhanced_with_owner_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ['vid' => '5', 'id' => $this->entities[2]->id()], + ['vid' => '6', 'id' => $this->entities[2]->id()], + ], ['vid' => 'vid']); + + // View own unpublished. + $user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '2', 'id' => $this->entities[0]->id()], + ['vid' => '3', 'id' => $this->entities[1]->id()], + ], ['vid' => 'vid']); + + // View own unpublished + view any (published-only). + $user = $this->createUser([], [ + 'view own unpublished entity_test_enhanced_with_owner', + 'view any entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[0]->set('user_id', $user->id()); + $this->entities[0]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '1', 'id' => $this->entities[0]->id()], + ['vid' => '2', 'id' => $this->entities[0]->id()], + ['vid' => '4', 'id' => $this->entities[1]->id()], + ['vid' => '5', 'id' => $this->entities[2]->id()], + ['vid' => '6', 'id' => $this->entities[2]->id()], + ], ['vid' => 'vid']); + + // View own $first_bundle + View any $second_bundle. + $user = $this->createUser([], [ + 'view own first entity_test_enhanced_with_owner', + 'view any second entity_test_enhanced_with_owner', + ]); + \Drupal::currentUser()->setAccount($user); + + $this->entities[1]->set('user_id', $user->id()); + $this->entities[1]->save(); + + $view = Views::getView('entity_test_enhanced_with_owner_revisions'); + $view->execute(); + $this->assertIdenticalResultset($view, [ + ['vid' => '4', 'id' => $this->entities[1]->id()], + ['vid' => '5', 'id' => $this->entities[2]->id()], + ['vid' => '6', 'id' => $this->entities[2]->id()], + ], ['vid' => 'vid']); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php b/web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php new file mode 100644 index 000000000..8d70bd2a8 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php @@ -0,0 +1,185 @@ +installEntitySchema('user'); + $this->installEntitySchema('entity_test_enhanced'); + $this->installSchema('system', 'router'); + $this->installConfig(['system']); + + \Drupal::service('router.builder')->rebuild(); + } + + /** + * Tests the revision history controller. + */ + public function testRevisionHistory() { + $entity = EnhancedEntity::create([ + 'name' => 'rev 1', + 'type' => 'default', + ]); + $entity->save(); + + $revision = clone $entity; + $revision->name->value = 'rev 2'; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(FALSE); + $revision->save(); + + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */ + $http_kernel = \Drupal::service('http_kernel'); + $request = Request::create($revision->toUrl('version-history')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(403, $response->getStatusCode()); + + $role_admin = Role::create(['id' => 'test_role_admin']); + $role_admin->grantPermission('administer entity_test_enhanced'); + $role_admin->save(); + + $role = Role::create(['id' => 'test_role']); + $role->grantPermission('view all entity_test_enhanced revisions'); + $role->grantPermission('administer entity_test_enhanced'); + $role->save(); + + $user_admin = User::create([ + 'name' => 'Test user admin', + ]); + $user_admin->addRole($role_admin->id()); + \Drupal::service('account_switcher')->switchTo($user_admin); + + $request = Request::create($revision->toUrl('version-history')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + + $user = User::create([ + 'name' => 'Test user', + ]); + $user->addRole($role->id()); + \Drupal::service('account_switcher')->switchTo($user); + + $request = Request::create($revision->toUrl('version-history')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + + // This ensures that the default revision is still the first revision. + $this->assertTrue(strpos($response->getContent(), 'entity_test_enhanced/1/revisions/2/view') !== FALSE); + $this->assertTrue(strpos($response->getContent(), 'entity_test_enhanced/1') !== FALSE); + + // Publish a new revision. + $revision = clone $entity; + $revision->name->value = 'rev 3'; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(TRUE); + $revision->save(); + + $request = Request::create($revision->toUrl('version-history')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + + // The first revision row should now include a revert link. + $this->assertTrue(strpos($response->getContent(), 'entity_test_enhanced/1/revisions/1/revert') !== FALSE); + } + + public function testRevisionView() { + $entity = EnhancedEntity::create([ + 'name' => 'rev 1', + 'type' => 'default', + ]); + $entity->save(); + + $revision = clone $entity; + $revision->name->value = 'rev 2'; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(FALSE); + $revision->save(); + + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */ + $http_kernel = \Drupal::service('http_kernel'); + $request = Request::create($revision->toUrl('revision')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(403, $response->getStatusCode()); + + $role_admin = Role::create(['id' => 'test_role_admin']); + $role_admin->grantPermission('administer entity_test_enhanced'); + $role_admin->save(); + + $role = Role::create(['id' => 'test_role']); + $role->grantPermission('view all entity_test_enhanced revisions'); + $role->grantPermission('administer entity_test_enhanced'); + $role->save(); + + $user_admin = User::create([ + 'name' => 'Test user admin', + ]); + $user_admin->addRole($role_admin->id()); + \Drupal::service('account_switcher')->switchTo($user_admin); + + $request = Request::create($revision->toUrl('version-history')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + + $user = User::create([ + 'name' => 'Test user', + ]); + $user->addRole($role->id()); + \Drupal::service('account_switcher')->switchTo($user); + + $request = Request::create($revision->toUrl('revision')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertNotContains('rev 1', $response->getContent()); + $this->assertContains('rev 2', $response->getContent()); + } + + public function testRevisionRevert() { + $entity = EnhancedEntity::create([ + 'name' => 'rev 1', + 'type' => 'entity_test_enhance', + ]); + $entity->save(); + $entity->name->value = 'rev 2'; + $entity->setNewRevision(TRUE); + $entity->isDefaultRevision(TRUE); + $entity->save(); + + $role = Role::create(['id' => 'test_role']); + $role->grantPermission('administer entity_test_enhanced'); + $role->grantPermission('revert all entity_test_enhanced revisions'); + $role->save(); + + $user = User::create([ + 'name' => 'Test user', + ]); + $user->addRole($role->id()); + \Drupal::service('account_switcher')->switchTo($user); + + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */ + $http_kernel = \Drupal::service('http_kernel'); + $request = Request::create($entity->toUrl('revision-revert-form')->toString()); + $response = $http_kernel->handle($request); + $this->assertEquals(200, $response->getStatusCode()); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Kernel/RevisionOverviewIntegrationTest.php b/web/modules/contrib/entity/tests/src/Kernel/RevisionOverviewIntegrationTest.php new file mode 100644 index 000000000..bc28c24e5 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Kernel/RevisionOverviewIntegrationTest.php @@ -0,0 +1,53 @@ +installSchema('system', 'router'); + + \Drupal::service('router.builder')->rebuild(); + } + + public function testIntegration() { + /** @var \Drupal\Core\Menu\LocalTaskManagerInterface $local_tasks_manager */ + $local_tasks_manager = \Drupal::service('plugin.manager.menu.local_task'); + + $tasks = $local_tasks_manager->getDefinitions(); + $this->assertArrayHasKey('entity.revisions_overview:entity_test_enhanced', $tasks); + $this->assertArrayNotHasKey('entity.revisions_overview:node', $tasks, 'Node should have been excluded because it provides their own'); + + $this->assertEquals('entity.entity_test_enhanced.version_history', $tasks['entity.revisions_overview:entity_test_enhanced']['route_name']); + $this->assertEquals('entity.entity_test_enhanced.canonical', $tasks['entity.revisions_overview:entity_test_enhanced']['base_route']); + + /** @var \Drupal\Core\Routing\RouteProviderInterface $route_provider */ + $route_provider = \Drupal::service('router.route_provider'); + + $route = $route_provider->getRouteByName('entity.entity_test_enhanced.version_history'); + $this->assertInstanceOf(Route::class, $route); + $this->assertEquals('\Drupal\entity\Controller\RevisionOverviewController::revisionOverviewController', $route->getDefault('_controller')); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Unit/BundleEntityAccessControlHandlerTest.php b/web/modules/contrib/entity/tests/src/Unit/BundleEntityAccessControlHandlerTest.php new file mode 100644 index 000000000..0126d4465 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Unit/BundleEntityAccessControlHandlerTest.php @@ -0,0 +1,115 @@ +prophesize(ModuleHandlerInterface::class); + $module_handler->invokeAll(Argument::any(), Argument::any())->willReturn([]); + $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); + $cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); + + $container = new ContainerBuilder(); + $container->set('module_handler', $module_handler->reveal()); + $container->set('cache_contexts_manager', $cache_contexts_manager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::checkAccess + * + * @dataProvider accessProvider + */ + public function testAccess(EntityInterface $entity, $operation, $account, $allowed) { + $handler = new BundleEntityAccessControlHandler($entity->getEntityType()); + $handler->setStringTranslation($this->getStringTranslationStub()); + $result = $handler->access($entity, $operation, $account); + $this->assertEquals($allowed, $result); + } + + /** + * Data provider for testAccess(). + * + * @return array + * A list of testAccess method arguments. + */ + public function accessProvider() { + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->id()->willReturn('green_entity_bundle'); + $entity_type->getBundleOf()->willReturn('green_entity'); + $entity_type->getAdminPermission()->willReturn('administer green_entity'); + $entity_type = $entity_type->reveal(); + + $entity = $this->prophesize(ConfigEntityInterface::class); + $entity->getEntityType()->willReturn($entity_type); + $entity->getEntityTypeId()->willReturn('green_entity_bundle'); + $entity->id()->willReturn('default'); + $entity->uuid()->willReturn('fake uuid'); + $entity->language()->willReturn(new Language(['id' => LanguageInterface::LANGCODE_NOT_SPECIFIED])); + + // User with no access. + $user = $this->buildMockUser(1, 'access content'); + $data[] = [$entity->reveal(), 'view label', $user->reveal(), FALSE]; + + // Permissions which grant "view label" access. + $permissions = [ + 'administer green_entity', + 'view green_entity', + 'view default green_entity', + 'view own green_entity', + 'view any green_entity', + 'view own default green_entity', + 'view any default green_entity', + ]; + foreach ($permissions as $index => $permission) { + $user = $this->buildMockUser(10 + $index, $permission); + $data[] = [$entity->reveal(), 'view label', $user->reveal(), TRUE]; + } + + return $data; + } + + /** + * Builds a mock user. + * + * @param int $uid + * The user ID. + * @param string $permission + * The permission to grant. + * + * @return \Prophecy\Prophecy\ObjectProphecy + * The user mock. + */ + protected function buildMockUser($uid, $permission) { + $account = $this->prophesize(AccountInterface::class); + $account->id()->willReturn($uid); + $account->hasPermission($permission)->willReturn(TRUE); + $account->hasPermission(Argument::any())->willReturn(FALSE); + + return $account; + } + +} diff --git a/web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php b/web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php new file mode 100644 index 000000000..dbae5dc41 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php @@ -0,0 +1,249 @@ +prophesize(ModuleHandlerInterface::class); + $module_handler->invokeAll(Argument::any(), Argument::any())->willReturn([]); + $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); + $cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); + + $container = new ContainerBuilder(); + $container->set('module_handler', $module_handler->reveal()); + $container->set('cache_contexts_manager', $cache_contexts_manager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::checkAccess + * @covers ::checkEntityPermissions + * @covers ::checkEntityOwnerPermissions + * @covers ::checkCreateAccess + * + * @dataProvider accessProvider + */ + public function testAccess(EntityInterface $entity, $operation, $account, $allowed) { + $handler = new EntityAccessControlHandler($entity->getEntityType()); + $handler->setStringTranslation($this->getStringTranslationStub()); + $result = $handler->access($entity, $operation, $account); + $this->assertEquals($allowed, $result); + } + + /** + * @covers ::checkCreateAccess + * + * @dataProvider createAccessProvider + */ + public function testCreateAccess(EntityTypeInterface $entity_type, $bundle, $account, $allowed) { + $handler = new EntityAccessControlHandler($entity_type); + $handler->setStringTranslation($this->getStringTranslationStub()); + $result = $handler->createAccess($bundle, $account); + $this->assertEquals($allowed, $result); + } + + /** + * Data provider for testAccess(). + * + * @return array + * A list of testAccess method arguments. + */ + public function accessProvider() { + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->id()->willReturn('green_entity'); + $entity_type->getAdminPermission()->willReturn('administer green_entity'); + $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); + $entity_type->getHandlerClass('permission_provider')->willReturn(EntityPermissionProvider::class); + $entity = $this->buildMockEntity($entity_type->reveal(), 6); + + $data = []; + // Admin permission. + $admin_user = $this->buildMockUser(5, 'administer green_entity'); + $data[] = [$entity->reveal(), 'view', $admin_user->reveal(), TRUE]; + $data[] = [$entity->reveal(), 'update', $admin_user->reveal(), TRUE]; + $data[] = [$entity->reveal(), 'delete', $admin_user->reveal(), TRUE]; + + // View, Update, delete permissions, entity without an owner. + $second_entity = $this->buildMockEntity($entity_type->reveal()); + foreach (['view', 'update', 'delete'] as $operation) { + $first_user = $this->buildMockUser(6, $operation . ' green_entity'); + $second_user = $this->buildMockUser(7, 'access content'); + + $data[] = [$second_entity->reveal(), $operation, $first_user->reveal(), TRUE]; + $data[] = [$second_entity->reveal(), $operation, $second_user->reveal(), FALSE]; + } + + // Update and delete permissions. + foreach (['update', 'delete'] as $operation) { + // Owner, non-owner, user with "any" permission. + $first_user = $this->buildMockUser(6, $operation . ' own green_entity'); + $second_user = $this->buildMockUser(7, $operation . ' own green_entity'); + $third_user = $this->buildMockUser(8, $operation . ' any green_entity'); + + $data[] = [$entity->reveal(), $operation, $first_user->reveal(), TRUE]; + $data[] = [$entity->reveal(), $operation, $second_user->reveal(), FALSE]; + $data[] = [$entity->reveal(), $operation, $third_user->reveal(), TRUE]; + } + + // View permissions. + $first_user = $this->buildMockUser(9, 'view green_entity'); + $second_user = $this->buildMockUser(10, 'view first green_entity'); + $third_user = $this->buildMockUser(14, 'view own unpublished green_entity'); + $fourth_user = $this->buildMockUser(14, 'access content'); + + $first_entity = $this->buildMockEntity($entity_type->reveal(), 1, 'first'); + $second_entity = $this->buildMockEntity($entity_type->reveal(), 1, 'second'); + $third_entity = $this->buildMockEntity($entity_type->reveal(), 14, 'first', FALSE); + + // The first user can view the two published entities. + $data[] = [$first_entity->reveal(), 'view', $first_user->reveal(), TRUE]; + $data[] = [$second_entity->reveal(), 'view', $first_user->reveal(), TRUE]; + $data[] = [$third_entity->reveal(), 'view', $first_user->reveal(), FALSE]; + + // The second user can only view published entities of bundle "first". + $data[] = [$first_entity->reveal(), 'view', $second_user->reveal(), TRUE]; + $data[] = [$second_entity->reveal(), 'view', $second_user->reveal(), FALSE]; + $data[] = [$third_entity->reveal(), 'view', $second_user->reveal(), FALSE]; + + // The third user can view their own unpublished entity. + $data[] = [$first_entity->reveal(), 'view', $third_user->reveal(), FALSE]; + $data[] = [$second_entity->reveal(), 'view', $third_user->reveal(), FALSE]; + $data[] = [$third_entity->reveal(), 'view', $third_user->reveal(), TRUE]; + + // The fourth user can't view anything. + $data[] = [$first_entity->reveal(), 'view', $fourth_user->reveal(), FALSE]; + $data[] = [$second_entity->reveal(), 'view', $fourth_user->reveal(), FALSE]; + $data[] = [$third_entity->reveal(), 'view', $fourth_user->reveal(), FALSE]; + + return $data; + } + + /** + * Data provider for testCreateAccess(). + * + * @return array + * A list of testCreateAccess method arguments. + */ + public function createAccessProvider() { + $data = []; + + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->id()->willReturn('green_entity'); + $entity_type->getAdminPermission()->willReturn('administer green_entity'); + $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); + $entity_type->getHandlerClass('permission_provider')->willReturn(EntityPermissionProvider::class); + + // User with the admin permission. + $account = $this->buildMockUser('6', 'administer green_entity'); + $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; + + // Ordinary user. + $account = $this->buildMockUser('6', 'create green_entity'); + $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; + + // Ordinary user, entity with a bundle. + $account = $this->buildMockUser('6', 'create first_bundle green_entity'); + $data[] = [$entity_type->reveal(), 'first_bundle', $account->reveal(), TRUE]; + + // User with no permissions. + $account = $this->buildMockUser('6', 'access content'); + $data[] = [$entity_type->reveal(), NULL, $account->reveal(), FALSE]; + + return $data; + } + + /** + * Builds a mock entity. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * @param string $owner_id + * The owner ID. + * @param string $bundle + * The bundle. + * @param bool $published + * Whether the entity is published. + * + * @return \Prophecy\Prophecy\ObjectProphecy + * The entity mock. + */ + protected function buildMockEntity(EntityTypeInterface $entity_type, $owner_id = NULL, $bundle = NULL, $published = NULL) { + $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED; + $entity = $this->prophesize(ContentEntityInterface::class); + if (isset($published)) { + $entity->willImplement(EntityPublishedInterface::class); + } + if ($owner_id) { + $entity->willImplement(EntityOwnerInterface::class); + } + if (isset($published)) { + $entity->isPublished()->willReturn($published); + } + if ($owner_id) { + $entity->getOwnerId()->willReturn($owner_id); + } + + $entity->bundle()->willReturn($bundle ?: $entity_type->id()); + $entity->isNew()->willReturn(FALSE); + $entity->uuid()->willReturn('fake uuid'); + $entity->id()->willReturn('fake id'); + $entity->getRevisionId()->willReturn(NULL); + $entity->language()->willReturn(new Language(['id' => $langcode])); + $entity->getEntityTypeId()->willReturn($entity_type->id()); + $entity->getEntityType()->willReturn($entity_type); + $entity->getCacheContexts()->willReturn([]); + $entity->getCacheTags()->willReturn([]); + $entity->getCacheMaxAge()->willReturn(Cache::PERMANENT); + + return $entity; + } + + /** + * Builds a mock user. + * + * @param int $uid + * The user ID. + * @param string $permission + * The permission to grant. + * + * @return \Prophecy\Prophecy\ObjectProphecy + * The user mock. + */ + protected function buildMockUser($uid, $permission) { + $account = $this->prophesize(AccountInterface::class); + $account->id()->willReturn($uid); + $account->hasPermission($permission)->willReturn(TRUE); + $account->hasPermission(Argument::any())->willReturn(FALSE); + + return $account; + } + +} diff --git a/web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php b/web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php new file mode 100644 index 000000000..9bb8842c3 --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php @@ -0,0 +1,185 @@ +prophesize(EntityTypeBundleInfoInterface::class); + $entity_type_bundle_info->getBundleInfo('white_entity')->willReturn([ + 'first' => ['label' => 'First'], + 'second' => ['label' => 'Second'], + ]); + $entity_type_bundle_info->getBundleInfo('black_entity')->willReturn([ + 'third' => ['label' => 'Third'], + ]); + $entity_type_bundle_info->getBundleInfo('pink_entity')->willReturn([ + 'third' => ['label' => 'Third'], + ]); + $this->permissionProvider = new EntityPermissionProvider($entity_type_bundle_info->reveal()); + $this->permissionProvider->setStringTranslation($this->getStringTranslationStub()); + } + + /** + * @covers ::buildPermissions + * + * @dataProvider entityTypeProvider + */ + public function testBuildPermissions(EntityTypeInterface $entity_type, array $expected_permissions) { + $permissions = $this->permissionProvider->buildPermissions($entity_type); + $this->assertEquals(array_keys($expected_permissions), array_keys($permissions)); + foreach ($permissions as $name => $permission) { + $this->assertEquals('entity_module_test', $permission['provider']); + $this->assertEquals($expected_permissions[$name], $permission['title']); + } + } + + /** + * Data provider for testBuildPermissions(). + * + * @return array + * A list of testBuildPermissions method arguments. + */ + public function entityTypeProvider() { + $data = []; + // Content entity type. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('green_entity'); + $entity_type->getSingularLabel()->willReturn('green entity'); + $entity_type->getPluralLabel()->willReturn('green entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(FALSE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('entity_type'); + $expected_permissions = [ + 'administer green_entity' => 'Administer green entities', + 'create green_entity' => 'Create green entities', + 'update green_entity' => 'Update green entities', + 'delete green_entity' => 'Delete green entities', + 'view green_entity' => 'View green entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with owner. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('blue_entity'); + $entity_type->getSingularLabel()->willReturn('blue entity'); + $entity_type->getPluralLabel()->willReturn('blue entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('entity_type'); + $expected_permissions = [ + 'administer blue_entity' => 'Administer blue entities', + 'access blue_entity overview' => 'Access the blue entities overview page', + 'create blue_entity' => 'Create blue entities', + 'update any blue_entity' => 'Update any blue entity', + 'update own blue_entity' => 'Update own blue entities', + 'delete any blue_entity' => 'Delete any blue entity', + 'delete own blue_entity' => 'Delete own blue entities', + 'view blue_entity' => 'View blue entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with bundles. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('white_entity'); + $entity_type->getSingularLabel()->willReturn('white entity'); + $entity_type->getPluralLabel()->willReturn('white entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('bundle'); + $expected_permissions = [ + 'administer white_entity' => 'Administer white entities', + 'access white_entity overview' => 'Access the white entities overview page', + 'create first white_entity' => 'First: Create white entities', + 'update first white_entity' => 'First: Update white entities', + 'delete first white_entity' => 'First: Delete white entities', + 'create second white_entity' => 'Second: Create white entities', + 'update second white_entity' => 'Second: Update white entities', + 'delete second white_entity' => 'Second: Delete white entities', + 'view white_entity' => 'View white entities', + 'view first white_entity' => 'First: View white entities', + 'view second white_entity' => 'Second: View white entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with bundles and owner. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('black_entity'); + $entity_type->getSingularLabel()->willReturn('black entity'); + $entity_type->getPluralLabel()->willReturn('black entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('bundle'); + $expected_permissions = [ + 'administer black_entity' => 'Administer black entities', + 'access black_entity overview' => 'Access the black entities overview page', + 'create third black_entity' => 'Third: Create black entities', + 'update any third black_entity' => 'Third: Update any black entity', + 'update own third black_entity' => 'Third: Update own black entities', + 'delete any third black_entity' => 'Third: Delete any black entity', + 'delete own third black_entity' => 'Third: Delete own black entities', + 'view black_entity' => 'View black entities', + 'view third black_entity' => 'Third: View black entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with bundles and owner and entity published. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('pink_entity'); + $entity_type->getSingularLabel()->willReturn('pink entity'); + $entity_type->getPluralLabel()->willReturn('pink entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(TRUE); + $entity_type->getPermissionGranularity()->willReturn('bundle'); + $expected_permissions = [ + 'administer pink_entity' => 'Administer pink entities', + 'access pink_entity overview' => 'Access the pink entities overview page', + 'view own unpublished pink_entity' => 'View own unpublished pink entities', + 'create third pink_entity' => 'Third: Create pink entities', + 'update any third pink_entity' => 'Third: Update any pink entity', + 'update own third pink_entity' => 'Third: Update own pink entities', + 'delete any third pink_entity' => 'Third: Delete any pink entity', + 'delete own third pink_entity' => 'Third: Delete own pink entities', + 'view pink_entity' => 'View pink entities', + 'view third pink_entity' => 'Third: View pink entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + return $data; + } + +} diff --git a/web/modules/contrib/entity/tests/src/Unit/QueryAccess/ConditionTest.php b/web/modules/contrib/entity/tests/src/Unit/QueryAccess/ConditionTest.php new file mode 100644 index 000000000..6d73aebaa --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Unit/QueryAccess/ConditionTest.php @@ -0,0 +1,50 @@ +assertEquals('uid', $condition->getField()); + $this->assertEquals('2', $condition->getValue()); + $this->assertEquals('=', $condition->getOperator()); + $this->assertEquals("uid = '2'", $condition->__toString()); + + $condition = new Condition('type', ['article', 'page']); + $this->assertEquals('type', $condition->getField()); + $this->assertEquals(['article', 'page'], $condition->getValue()); + $this->assertEquals('IN', $condition->getOperator()); + $this->assertEquals("type IN ['article', 'page']", $condition->__toString()); + + $condition = new Condition('title', NULL, 'IS NULL'); + $this->assertEquals('title', $condition->getField()); + $this->assertEquals(NULL, $condition->getValue()); + $this->assertEquals('IS NULL', $condition->getOperator()); + $this->assertEquals("title IS NULL", $condition->__toString()); + } + +} diff --git a/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php b/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php new file mode 100644 index 000000000..1cc84676d --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php @@ -0,0 +1,253 @@ +prophesize(ModuleHandlerInterface::class); + $module_handler->invokeAll(Argument::any(), Argument::any())->willReturn([]); + $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); + $cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); + + $container = new ContainerBuilder(); + $container->set('module_handler', $module_handler->reveal()); + $container->set('cache_contexts_manager', $cache_contexts_manager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::checkAccess + * @covers ::checkEntityPermissions + * @covers ::checkEntityOwnerPermissions + * @covers ::checkCreateAccess + * + * @dataProvider accessProvider + */ + public function testAccess(EntityInterface $entity, $operation, $account, $allowed) { + $handler = new UncacheableEntityAccessControlHandler($entity->getEntityType()); + $handler->setStringTranslation($this->getStringTranslationStub()); + $result = $handler->access($entity, $operation, $account); + $this->assertEquals($allowed, $result); + } + + /** + * @covers ::checkCreateAccess + * + * @dataProvider createAccessProvider + */ + public function testCreateAccess(EntityTypeInterface $entity_type, $bundle, $account, $allowed) { + $handler = new UncacheableEntityAccessControlHandler($entity_type); + $handler->setStringTranslation($this->getStringTranslationStub()); + $result = $handler->createAccess($bundle, $account); + $this->assertEquals($allowed, $result); + } + + /** + * Data provider for testAccess(). + * + * @return array + * A list of testAccess method arguments. + */ + public function accessProvider() { + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->id()->willReturn('green_entity'); + $entity_type->getAdminPermission()->willReturn('administer green_entity'); + $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); + $entity_type->getHandlerClass('permission_provider')->willReturn(UncacheableEntityPermissionProvider::class); + $entity = $this->buildMockEntity($entity_type->reveal(), 6); + + $data = []; + // Admin permission. + $admin_user = $this->buildMockUser(5, 'administer green_entity'); + $data[] = [$entity->reveal(), 'view', $admin_user->reveal(), TRUE]; + $data[] = [$entity->reveal(), 'update', $admin_user->reveal(), TRUE]; + $data[] = [$entity->reveal(), 'delete', $admin_user->reveal(), TRUE]; + + // View, update, delete permissions, entity without an owner. + $second_entity = $this->buildMockEntity($entity_type->reveal()); + foreach (['view', 'update', 'delete'] as $operation) { + $first_user = $this->buildMockUser(6, $operation . ' green_entity'); + $second_user = $this->buildMockUser(7, 'access content'); + + $data[] = [$second_entity->reveal(), $operation, $first_user->reveal(), TRUE]; + $data[] = [$second_entity->reveal(), $operation, $second_user->reveal(), FALSE]; + } + + // View, update, delete permissions. + foreach (['view', 'update', 'delete'] as $operation) { + // Owner, non-owner, user with "any" permission. + $first_user = $this->buildMockUser(6, $operation . ' own green_entity'); + $second_user = $this->buildMockUser(7, $operation . ' own green_entity'); + $third_user = $this->buildMockUser(8, $operation . ' any green_entity'); + + $data[] = [$entity->reveal(), $operation, $first_user->reveal(), TRUE]; + $data[] = [$entity->reveal(), $operation, $second_user->reveal(), FALSE]; + $data[] = [$entity->reveal(), $operation, $third_user->reveal(), TRUE]; + } + + // Per bundle and unpublished view permissions. + $first_user = $this->buildMockUser(11, 'view any first green_entity'); + $second_user = $this->buildMockUser(12, 'view own first green_entity'); + $third_user = $this->buildMockUser(13, 'view own unpublished green_entity'); + + $first_entity = $this->buildMockEntity($entity_type->reveal(), 9999, 'first'); + $second_entity = $this->buildMockEntity($entity_type->reveal(), 12, 'first'); + $third_entity = $this->buildMockEntity($entity_type->reveal(), 9999, 'second'); + $fourth_entity = $this->buildMockEntity($entity_type->reveal(), 10, 'second'); + $fifth_entity = $this->buildMockEntity($entity_type->reveal(), 13, 'first', FALSE); + + // The first user can view the two entities of bundle "first". + $data[] = [$first_entity->reveal(), 'view', $first_user->reveal(), TRUE]; + $data[] = [$second_entity->reveal(), 'view', $first_user->reveal(), TRUE]; + $data[] = [$third_entity->reveal(), 'view', $first_user->reveal(), FALSE]; + $data[] = [$fourth_entity->reveal(), 'view', $first_user->reveal(), FALSE]; + $data[] = [$fifth_entity->reveal(), 'view', $first_user->reveal(), FALSE]; + + // The second user can view their own entity of bundle "first". + $data[] = [$first_entity->reveal(), 'view', $second_user->reveal(), FALSE]; + $data[] = [$second_entity->reveal(), 'view', $second_user->reveal(), TRUE]; + $data[] = [$third_entity->reveal(), 'view', $second_user->reveal(), FALSE]; + $data[] = [$fourth_entity->reveal(), 'view', $second_user->reveal(), FALSE]; + $data[] = [$fourth_entity->reveal(), 'view', $second_user->reveal(), FALSE]; + $data[] = [$fifth_entity->reveal(), 'view', $second_user->reveal(), FALSE]; + + // The third user can only view their own unpublished entity. + $data[] = [$first_entity->reveal(), 'view', $third_user->reveal(), FALSE]; + $data[] = [$second_entity->reveal(), 'view', $third_user->reveal(), FALSE]; + $data[] = [$third_entity->reveal(), 'view', $third_user->reveal(), FALSE]; + $data[] = [$fourth_entity->reveal(), 'view', $third_user->reveal(), FALSE]; + $data[] = [$fourth_entity->reveal(), 'view', $third_user->reveal(), FALSE]; + $data[] = [$fifth_entity->reveal(), 'view', $third_user->reveal(), TRUE]; + + return $data; + } + + /** + * Data provider for testCreateAccess(). + * + * @return array + * A list of testCreateAccess method arguments. + */ + public function createAccessProvider() { + $data = []; + + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->id()->willReturn('green_entity'); + $entity_type->getAdminPermission()->willReturn('administer green_entity'); + $entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE); + $entity_type->getHandlerClass('permission_provider')->willReturn(UncacheableEntityPermissionProvider::class); + + // User with the admin permission. + $account = $this->buildMockUser('6', 'administer green_entity'); + $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; + + // Ordinary user. + $account = $this->buildMockUser('6', 'create green_entity'); + $data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE]; + + // Ordinary user, entity with a bundle. + $account = $this->buildMockUser('6', 'create first_bundle green_entity'); + $data[] = [$entity_type->reveal(), 'first_bundle', $account->reveal(), TRUE]; + + // User with no permissions. + $account = $this->buildMockUser('6', 'access content'); + $data[] = [$entity_type->reveal(), NULL, $account->reveal(), FALSE]; + + return $data; + } + + /** + * Builds a mock entity. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * @param string $owner_id + * The owner ID. + * @param string $bundle + * The bundle. + * @param bool $published + * Whether the entity is published. + * + * @return \Prophecy\Prophecy\ObjectProphecy + * The entity mock. + */ + protected function buildMockEntity(EntityTypeInterface $entity_type, $owner_id = NULL, $bundle = NULL, $published = NULL) { + $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED; + $entity = $this->prophesize(ContentEntityInterface::class); + if (isset($published)) { + $entity->willImplement(EntityPublishedInterface::class); + } + if ($owner_id) { + $entity->willImplement(EntityOwnerInterface::class); + } + if (isset($published)) { + $entity->isPublished()->willReturn($published); + } + if ($owner_id) { + $entity->getOwnerId()->willReturn($owner_id); + } + + $entity->bundle()->willReturn($bundle ?: $entity_type->id()); + $entity->isNew()->willReturn(FALSE); + $entity->uuid()->willReturn('fake uuid'); + $entity->id()->willReturn('fake id'); + $entity->getRevisionId()->willReturn(NULL); + $entity->language()->willReturn(new Language(['id' => $langcode])); + $entity->getEntityTypeId()->willReturn($entity_type->id()); + $entity->getEntityType()->willReturn($entity_type); + $entity->getCacheContexts()->willReturn([]); + $entity->getCacheTags()->willReturn([]); + $entity->getCacheMaxAge()->willReturn(Cache::PERMANENT); + + return $entity; + } + + /** + * Builds a mock user. + * + * @param int $uid + * The user ID. + * @param string $permission + * The permission to grant. + * + * @return \Prophecy\Prophecy\ObjectProphecy + * The user mock. + */ + protected function buildMockUser($uid, $permission) { + $account = $this->prophesize(AccountInterface::class); + $account->id()->willReturn($uid); + $account->hasPermission($permission)->willReturn(TRUE); + $account->hasPermission(Argument::any())->willReturn(FALSE); + + return $account; + } + +} diff --git a/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php b/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php new file mode 100644 index 000000000..45cf0fd5e --- /dev/null +++ b/web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php @@ -0,0 +1,190 @@ +prophesize(EntityTypeBundleInfoInterface::class); + $entity_type_bundle_info->getBundleInfo('white_entity')->willReturn([ + 'first' => ['label' => 'First'], + 'second' => ['label' => 'Second'], + ]); + $entity_type_bundle_info->getBundleInfo('black_entity')->willReturn([ + 'third' => ['label' => 'Third'], + ]); + $entity_type_bundle_info->getBundleInfo('pink_entity')->willReturn([ + 'third' => ['label' => 'Third'], + ]); + $this->permissionProvider = new UncacheableEntityPermissionProvider($entity_type_bundle_info->reveal()); + $this->permissionProvider->setStringTranslation($this->getStringTranslationStub()); + } + + /** + * @covers ::buildPermissions + * + * @dataProvider entityTypeProvider + */ + public function testBuildPermissions(EntityTypeInterface $entity_type, array $expected_permissions) { + $permissions = $this->permissionProvider->buildPermissions($entity_type); + $this->assertEquals(array_keys($expected_permissions), array_keys($permissions)); + foreach ($permissions as $name => $permission) { + $this->assertEquals('entity_module_test', $permission['provider']); + $this->assertEquals($expected_permissions[$name], $permission['title']); + } + } + + /** + * Data provider for testBuildPermissions(). + * + * @return array + * A list of testBuildPermissions method arguments. + */ + public function entityTypeProvider() { + $data = []; + // Content entity type. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('green_entity'); + $entity_type->getSingularLabel()->willReturn('green entity'); + $entity_type->getPluralLabel()->willReturn('green entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(FALSE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('entity_type'); + $expected_permissions = [ + 'administer green_entity' => 'Administer green entities', + 'create green_entity' => 'Create green entities', + 'update green_entity' => 'Update green entities', + 'delete green_entity' => 'Delete green entities', + 'view green_entity' => 'View green entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with owner. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('blue_entity'); + $entity_type->getSingularLabel()->willReturn('blue entity'); + $entity_type->getPluralLabel()->willReturn('blue entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('entity_type'); + $expected_permissions = [ + 'administer blue_entity' => 'Administer blue entities', + 'access blue_entity overview' => 'Access the blue entities overview page', + 'create blue_entity' => 'Create blue entities', + 'update any blue_entity' => 'Update any blue entity', + 'update own blue_entity' => 'Update own blue entities', + 'delete any blue_entity' => 'Delete any blue entity', + 'delete own blue_entity' => 'Delete own blue entities', + 'view any blue_entity' => 'View any blue entities', + 'view own blue_entity' => 'View own blue entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with bundles. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('white_entity'); + $entity_type->getSingularLabel()->willReturn('white entity'); + $entity_type->getPluralLabel()->willReturn('white entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('bundle'); + $expected_permissions = [ + 'administer white_entity' => 'Administer white entities', + 'access white_entity overview' => 'Access the white entities overview page', + 'create first white_entity' => 'First: Create white entities', + 'update first white_entity' => 'First: Update white entities', + 'delete first white_entity' => 'First: Delete white entities', + 'create second white_entity' => 'Second: Create white entities', + 'update second white_entity' => 'Second: Update white entities', + 'delete second white_entity' => 'Second: Delete white entities', + 'view white_entity' => 'View white entities', + 'view first white_entity' => 'First: View white entities', + 'view second white_entity' => 'Second: View white entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with bundles and owner. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('black_entity'); + $entity_type->getSingularLabel()->willReturn('black entity'); + $entity_type->getPluralLabel()->willReturn('black entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE); + $entity_type->getPermissionGranularity()->willReturn('bundle'); + $expected_permissions = [ + 'administer black_entity' => 'Administer black entities', + 'access black_entity overview' => 'Access the black entities overview page', + 'create third black_entity' => 'Third: Create black entities', + 'update any third black_entity' => 'Third: Update any black entity', + 'update own third black_entity' => 'Third: Update own black entities', + 'delete any third black_entity' => 'Third: Delete any black entity', + 'delete own third black_entity' => 'Third: Delete own black entities', + 'view any black_entity' => 'View any black entities', + 'view own black_entity' => 'View own black entities', + 'view any third black_entity' => 'Third: View any black entities', + 'view own third black_entity' => 'Third: View own black entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + // Content entity type with bundles and owner and entity published. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_type->getProvider()->willReturn('entity_module_test'); + $entity_type->id()->willReturn('pink_entity'); + $entity_type->getSingularLabel()->willReturn('pink entity'); + $entity_type->getPluralLabel()->willReturn('pink entities'); + $entity_type->hasLinkTemplate('collection')->willReturn(TRUE); + $entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE); + $entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(TRUE); + $entity_type->getPermissionGranularity()->willReturn('bundle'); + $expected_permissions = [ + 'administer pink_entity' => 'Administer pink entities', + 'access pink_entity overview' => 'Access the pink entities overview page', + 'view own unpublished pink_entity' => 'View own unpublished pink entities', + 'create third pink_entity' => 'Third: Create pink entities', + 'update any third pink_entity' => 'Third: Update any pink entity', + 'update own third pink_entity' => 'Third: Update own pink entities', + 'delete any third pink_entity' => 'Third: Delete any pink entity', + 'delete own third pink_entity' => 'Third: Delete own pink entities', + 'view any pink_entity' => 'View any pink entities', + 'view own pink_entity' => 'View own pink entities', + 'view any third pink_entity' => 'Third: View any pink entities', + 'view own third pink_entity' => 'Third: View own pink entities', + ]; + $data[] = [$entity_type->reveal(), $expected_permissions]; + + return $data; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/LICENSE.txt b/web/modules/contrib/entity_reference_revisions/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/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/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml b/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml new file mode 100644 index 000000000..75a216aec --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml @@ -0,0 +1,69 @@ +# Schema for the configuration files of the Entity Reference module. + +entity_reference_revisions.default.handler_settings: + type: mapping + label: 'View handler settings' + mapping: + target_bundles: + type: sequence + label: 'types' + sequence: + - type: string + label: 'Type' + sort: + type: mapping + label: 'Sort settings' + mapping: + field: + type: string + label: 'Sort by' + direction: + type: string + label: 'Sort direction' + filter: + type: mapping + label: 'Filter settings' + mapping: + type: + type: string + label: 'Filter by' + role: + type: sequence + label: 'Restrict to the selected roles' + sequence: + - type: string + label: 'Role' + auto_create: + type: boolean + label: 'Create referenced entities if they don''t already exist' + +field.storage_settings.entity_reference_revisions: + type: field.storage_settings.entity_reference + +field.value.entity_reference_revisions: + type: field.value.entity_reference + label: 'Default value' + mapping: + target_id: + type: integer + label: 'Value' + target_revision_id: + type: integer + label: 'Revision ID' + target_uuid: + type: string + label: 'Target UUID' + +field.field_settings.entity_reference_revisions: + type: field.field_settings.entity_reference + +field.widget.settings.entity_reference_revisions_autocomplete: + type: field.widget.settings.entity_reference_autocomplete + +field.formatter.settings.entity_reference_revisions_entity_view: + type: mapping + mapping: + view_mode: + type: string + link: + type: string diff --git a/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml b/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml new file mode 100644 index 000000000..afbad6375 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml @@ -0,0 +1,20 @@ +# Schema for the views plugins of the Entity Reference module. + +views.display.entity_reference_revisions: + type: views_display + label: 'Entity Reference Revisions' + +views.row.entity_reference_revisions: + type: views.row.fields + label: 'Entity Reference Revisions inline fields' + +views.style.entity_reference_revisions: + type: views_style + label: 'Entity Reference Revisions list' + mapping: + search_fields: + type: sequence + label: 'Search fields' + sequence: + - type: string + label: 'Search field' diff --git a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml new file mode 100644 index 000000000..05a3b279c --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml @@ -0,0 +1,14 @@ +name: Entity Reference Revisions +type: module +description: Adds a Entity Reference field type with revision support. +# core: 8.x +package: Field types + +test_dependencies: + - diff:diff + +# Information added by Drupal.org packaging script on 2018-10-15 +version: '8.x-1.6' +core: '8.x' +project: 'entity_reference_revisions' +datestamp: 1539588784 diff --git a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module new file mode 100644 index 000000000..cead27bf1 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module @@ -0,0 +1,282 @@ +' . t('About') . ''; + $output .= '

' . t('The Entity Reference Revisions module allows you to create fields that contain links to other entities (such as content items, taxonomy terms, etc.) within the site. This allows you, for example, to include a link to a user within a content item. For more information, see the online documentation for the Entity Reference Revisions module and the Field module help page.', [':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':er_do' => 'https://drupal.org/documentation/modules/entity_reference_revisions']) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Managing and displaying entity reference fields') . '
'; + $output .= '
' . t('The settings and the display of the entity reference field can be configured separately. See the Field UI help for more information on how to manage fields and their display.', [':field_ui' => Url::fromRoute('help.page', ['name' => 'field_ui'])->toString()]) . '
'; + $output .= '
' . t('Selecting reference type') . '
'; + $output .= '
' . t('In the field settings you can select which entity type you want to create a reference to.') . '
'; + $output .= '
' . t('Filtering and sorting reference fields') . '
'; + $output .= '
' . t('Depending on the chosen entity type, additional filtering and sorting options are available for the list of entities that can be referred to, in the field settings. For example, the list of users can be filtered by role and sorted by name or ID.') . '
'; + $output .= '
' . t('Displaying a reference') . '
'; + $output .= '
' . t('An entity reference can be displayed as a simple label with or without a link to the entity. Alternatively, the referenced entity can be displayed as a teaser (or any other available view mode) inside the referencing entity.') . '
'; + $output .= '
'; + return $output; + } +} + +/** + * Implements hook_field_widget_info_alter(). + */ +function entity_reference_revisions_field_widget_info_alter(&$info) { + if (isset($info['options_select'])) { + $info['options_select']['field_types'][] = 'entity_reference_revisions'; + } + if (isset($info['options_buttons'])) { + $info['options_buttons']['field_types'][] = 'entity_reference_revisions'; + } +} + +/** + * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'. + * + * Reset the instance handler settings, when the target type is changed. + */ +function entity_reference_revisions_field_storage_config_update(FieldStorageConfigInterface $field_storage) { + if ($field_storage->getType() != 'entity_reference_revisions') { + // Only act on entity reference fields. + return; + } + + if ($field_storage->isSyncing()) { + // Don't change anything during a configuration sync. + return; + } + + if ($field_storage->getSetting('target_type') == $field_storage->original->getSetting('target_type')) { + // Target type didn't change. + return; + } + + if (empty($field_storage->bundles)) { + // Field storage has no fields. + return; + } + + $field_name = $field_storage->getName(); + + foreach ($field_storage->bundles() as $entity_type => $bundles) { + foreach ($bundles as $bundle) { + $field = FieldConfig::loadByName($entity_type, $bundle, $field_name); + $field->setSetting('handler_settings', []); + $field->save(); + } + } +} + +/** + * Render API callback: Processes the field settings form and allows access to + * the form state. + * + * @see entity_reference_revisions_field_field_settings_form() + */ +function _entity_reference_revisions_field_field_settings_ajax_process($form, FormStateInterface $form_state) { + _entity_reference_revisions_field_field_settings_ajax_process_element($form, $form); + return $form; +} + +/** + * Adds entity_reference specific properties to AJAX form elements from the + * field settings form. + * + * @see _entity_reference_revisions_field_field_settings_ajax_process() + */ +function _entity_reference_revisions_field_field_settings_ajax_process_element(&$element, $main_form) { + if (!empty($element['#ajax'])) { + $element['#ajax'] = [ + 'callback' => 'entity_reference_revisions_settings_ajax', + 'wrapper' => $main_form['#id'], + 'element' => $main_form['#array_parents'], + ]; + } + + foreach (Element::children($element) as $key) { + _entity_reference_revisions_field_field_settings_ajax_process_element($element[$key], $main_form); + } +} + +/** + * Render API callback: Moves entity_reference specific Form API elements + * (i.e. 'handler_settings') up a level for easier processing by the validation + * and submission handlers. + * + * @see _entity_reference_revisions_field_settings_process() + */ +function _entity_reference_revisions_form_process_merge_parent($element) { + $parents = $element['#parents']; + array_pop($parents); + $element['#parents'] = $parents; + return $element; +} + +/** + * Form element validation handler; Filters the #value property of an element. + */ +function _entity_reference_revisions_element_validate_filter(&$element, FormStateInterface $form_state) { + $element['#value'] = array_filter($element['#value']); + $form_state->setValueForElement($element, $element['#value']); +} + +/** + * Ajax callback for the handler settings form. + * + * @see entity_reference_revisions_field_field_settings_form() + */ +function entity_reference_revisions_settings_ajax($form, FormStateInterface $form_state) { + return NestedArray::getValue($form, $form_state->getTriggeringElement()['#ajax']['element']); +} + +/** + * Submit handler for the non-JS case. + * + * @see entity_reference_revisions_field_field_settings_form() + */ +function entity_reference_revisions_settings_ajax_submit($form, FormStateInterface $form_state) { + $form_state->setRebuild(); +} + +/** + * Creates a field of an entity reference revisions field storage on the specified bundle. + * + * @param string $entity_type + * The type of entity the field will be attached to. + * @param string $bundle + * The bundle name of the entity the field will be attached to. + * @param string $field_name + * The name of the field; if it already exists, a new instance of the existing + * field will be created. + * @param string $field_label + * The label of the field. + * @param string $target_entity_type + * The type of the referenced entity. + * @param string $selection_handler + * The selection handler used by this field. + * @param array $selection_handler_settings + * An array of settings supported by the selection handler specified above. + * (e.g. 'target_bundles', 'sort', 'auto_create', etc). + * @param int $cardinality + * The cardinality of the field. + * + * @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase::buildConfigurationForm() + */ +function entity_reference_revisions_create_field($entity_type, $bundle, $field_name, $field_label, $target_entity_type, $selection_handler = 'default', $selection_handler_settings = [], $cardinality = 1) { + // Look for or add the specified field to the requested entity bundle. + if (!FieldStorageConfig::loadByName($entity_type, $field_name)) { + \Drupal::entityTypeManager()->getStorage('field_storage_config')->create([ + 'field_name' => $field_name, + 'type' => 'entity_reference_revisions', + 'entity_type' => $entity_type, + 'cardinality' => $cardinality, + 'settings' => [ + 'target_type' => $target_entity_type, + ], + ])->save(); + } + if (!FieldConfig::loadByName($entity_type, $bundle, $field_name)) { + \Drupal::entityTypeManager()->getStorage('field_config')->create([ + 'field_name' => $field_name, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'label' => $field_label, + 'settings' => [ + 'handler' => $selection_handler, + 'handler_settings' => $selection_handler_settings, + ], + ])->save(); + } +} + +/** + * Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'. + */ +function entity_reference_revisions_form_field_ui_field_storage_add_form_alter(array &$form) { + // Move the "Entity reference revisions" option to the end of the list and rename it to + // "Other". + unset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions']); + $form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions'] = t('Other…'); +} + +/** + * Implements hook_entity_revision_create(). + */ +function entity_reference_revisions_entity_revision_create(ContentEntityInterface $new_revision, ContentEntityInterface $entity, $keep_untranslatable_fields) { + $entity_type_manager = \Drupal::entityTypeManager(); + $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); + foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { + if ($field_definition->getType() == 'entity_reference_revisions' && !$field_definition->isTranslatable()) { + $target_entity_type_id = $field_definition->getSetting('target_type'); + if ($entity_type_manager->getDefinition($target_entity_type_id)->get('entity_revision_parent_id_field')) { + + // The default implementation copied the values from the current + // default revision into the field since it is not translatable. + // Take the originally referenced entity, create a new revision + // of it and set that instead on the new entity revision. + $active_langcode = $entity->language()->getId(); + $target_storage = \Drupal::entityTypeManager()->getStorage($target_entity_type_id); + if ($target_storage instanceof TranslatableRevisionableStorageInterface) { + + $items = $entity->get($field_name); + $translation_items = NULL; + if (!$new_revision->isDefaultTranslation() && $storage instanceof TranslatableRevisionableStorageInterface) { + $translation_items = $items; + $items = $storage->load($new_revision->id())->get($field_name); + } + + $values = []; + foreach ($items as $delta => $item) { + // Use the item from the translation if it exists. + // If we have translation items, use that if one with the matching + // target id exists. + if ($translation_items) { + foreach ($translation_items as $translation_item) { + if ($item->target_id == $translation_item->target_id) { + $item = $translation_item; + break; + } + } + } + + /** @var \Drupal\Core\Entity\ContentEntityInterface $target_entity */ + $target_entity = $item->entity; + if (!$target_entity->hasTranslation($active_langcode)) { + $target_entity->addTranslation($active_langcode, $target_entity->toArray()); + } + $target_entity = $item->entity->getTranslation($active_langcode); + $revised_entity = $target_storage->createRevision($target_entity, $new_revision->isDefaultRevision(), $keep_untranslatable_fields); + + // Restore the revision ID. + $revision_key = $revised_entity->getEntityType()->getKey('revision'); + $revised_entity->set($revision_key, $revised_entity->getLoadedRevisionId()); + $values[$delta] = $revised_entity; + } + $new_revision->set($field_name, $values); + } + } + } + } +} diff --git a/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc new file mode 100644 index 000000000..9d351b376 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc @@ -0,0 +1,73 @@ + $table_data) { + // Add a relationship to the target entity type. + $target_entity_type_id = $field_storage->getSetting('target_type'); + $target_entity_type = $entity_manager->getDefinition($target_entity_type_id); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + $entity_type = $entity_manager->getDefinition($entity_type_id); + $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable(); + $field_name = $field_storage->getName(); + + // Provide a relationship for the entity type with the entity reference + // revisions field. + $args = array( + '@label' => $target_entity_type->getLabel(), + '@field_name' => $field_name, + ); + $data[$table_name][$field_name]['relationship'] = array( + 'title' => t('@label referenced from @field_name', $args), + 'label' => t('@field_name: @label', $args), + 'group' => $entity_type->getLabel(), + 'help' => t('Appears in: @bundles.', array('@bundles' => implode(', ', $field_storage->getBundles()))), + 'id' => 'standard', + 'base' => $target_base_table, + 'entity type' => $target_entity_type_id, + 'base field' => $target_entity_type->getKey('revision'), + 'relationship field' => $field_name . '_target_revision_id', + ); + + // Provide a reverse relationship for the entity type that is referenced by + // the field. + $args['@entity'] = $entity_type->getLabel(); + $args['@label'] = $target_entity_type->getLowercaseLabel(); + $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); + $data[$target_base_table][$pseudo_field_name]['relationship'] = array( + 'title' => t('@entity using @field_name', $args), + 'label' => t('@field_name', array('@field_name' => $field_name)), + 'group' => $target_entity_type->getLabel(), + 'help' => t('Relate each @entity with a @field_name set to the @label.', $args), + 'id' => 'entity_reverse', + 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), + 'entity_type' => $entity_type_id, + 'base field' => $entity_type->getKey('revision'), + 'field_name' => $field_name, + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), + 'field field' => $field_name . '_target_revision_id', + 'join_extra' => array( + array( + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ), + ), + ); + } + + return $data; +} diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php b/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php new file mode 100644 index 000000000..1513aa3dd --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php @@ -0,0 +1,17 @@ +needsSave; + } + + /** + * {@inheritdoc} + */ + public function setNeedsSave($needs_save) { + $this->needsSave = $needs_save; + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + $this->setNeedsSave(FALSE); + } +} diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php b/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php new file mode 100644 index 000000000..983e7be26 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php @@ -0,0 +1,151 @@ +list)) { + return array(); + } + + // Collect the IDs of existing entities to load, and directly grab the + // "autocreate" entities that are already populated in $item->entity. + $target_entities = $ids = array(); + foreach ($this->list as $delta => $item) { + if ($item->hasNewEntity()) { + $target_entities[$delta] = $item->entity; + } + elseif ($item->target_revision_id !== NULL) { + $ids[$delta] = $item->target_revision_id; + } + } + + // Load and add the existing entities. + if ($ids) { + $target_type = $this->getFieldDefinition()->getSetting('target_type'); + foreach ($ids as $delta => $target_id) { + $entity = \Drupal::entityTypeManager()->getStorage($target_type)->loadRevision($target_id); + if ($entity) { + $target_entities[$delta] = $entity; + } + } + // Ensure the returned array is ordered by deltas. + ksort($target_entities); + } + + return $target_entities; + } + + /** + * {@inheritdoc} + */ + public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition) { + $default_value = parent::processDefaultValue($default_value, $entity, $definition); + + if ($default_value) { + // Convert UUIDs to numeric IDs. + $uuids = array(); + foreach ($default_value as $delta => $properties) { + if (isset($properties['target_uuid'])) { + $uuids[$delta] = $properties['target_uuid']; + } + } + if ($uuids) { + $target_type = $definition->getSetting('target_type'); + $entity_ids = \Drupal::entityQuery($target_type) + ->condition('uuid', $uuids, 'IN') + ->execute(); + $entities = \Drupal::entityTypeManager() + ->getStorage($target_type) + ->loadMultiple($entity_ids); + + $entity_uuids = array(); + foreach ($entities as $id => $entity) { + $entity_uuids[$entity->uuid()] = $id; + } + foreach ($uuids as $delta => $uuid) { + if (isset($entity_uuids[$uuid])) { + $default_value[$delta]['target_id'] = $entity_uuids[$uuid]; + unset($default_value[$delta]['target_uuid']); + } + else { + unset($default_value[$delta]); + } + } + } + + // Ensure we return consecutive deltas, in case we removed unknown UUIDs. + $default_value = array_values($default_value); + } + + return $default_value; + } + + /** + * {@inheritdoc} + */ + public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) { + $default_value = parent::defaultValuesFormSubmit($element, $form, $form_state); + + // Convert numeric IDs to UUIDs to ensure config deployability. + $ids = array(); + foreach ($default_value as $delta => $properties) { + $ids[] = $properties['target_revision_id']; + } + + $entities = array(); + foreach($ids as $id) { + $entities[$id] = \Drupal::entityTypeManager() + ->getStorage($this->getSetting('target_type')) + ->loadRevision($id); + } + + foreach ($default_value as $delta => $properties) { + $default_value[$delta] = array( + 'target_uuid' => $entities[$properties['target_revision_id']]->uuid(), + 'target_revision_id' => $properties['target_revision_id'], + ); + } + return $default_value; + } + + /** + * {@inheritdoc} + */ + public function hasAffectingChanges(FieldItemListInterface $original_items, $langcode) { + // If there are fewer items, then it is a change. + if (count($this) < count($original_items)) { + return TRUE; + } + + foreach ($this as $delta => $item) { + // If this is a different entity, then it is an affecting change. + if (!$original_items->offsetExists($delta) || $item->target_id != $original_items[$delta]->target_id) { + return TRUE; + } + // If it is the same entity, only consider it as having affecting changes + // if the target entity itself has changes. + if ($item->entity && $item->entity->hasTranslation($langcode) && $item->entity->getTranslation($langcode)->hasTranslationChanges()) { + return TRUE; + } + } + + return FALSE; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php b/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php new file mode 100644 index 000000000..65db78508 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php @@ -0,0 +1,35 @@ +getParameter('container.modules'); + if (isset($modules['hal'])) { + // Hal module is enabled, add our new normalizer for entity reference + // revision items. + $service_definition = new Definition('Drupal\entity_reference_revisions\Normalizer\EntityReferenceRevisionItemNormalizer', array( + new Reference('hal.link_manager'), + new Reference('serializer.entity_resolver'), + )); + // The priority must be higher than that of + // serializer.normalizer.entity_reference_item.hal in + // hal.services.yml. + $service_definition->addTag('normalizer', array('priority' => 20)); + $container->setDefinition('serializer.normalizer.entity_reference_revision_item', $service_definition); + } + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php b/web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php new file mode 100644 index 000000000..5c63cf2df --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php @@ -0,0 +1,43 @@ +getParent()->getName(); + $entity = $field_item->getEntity(); + $field_uri = $this->linkManager->getRelationUri($entity->getEntityTypeId(), $entity->bundle(), $field_name, $context); + $data['_embedded'][$field_uri][0]['target_revision_id'] = $field_item->target_revision_id; + return $data; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php new file mode 100644 index 000000000..b24b6b107 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php @@ -0,0 +1,128 @@ +addConstraint('Bundle', $bundle); + * \Drupal\Core\TypedData\DataReferenceDefinition::create('entity_revision') + * ->setTargetDefinition($definition); + * @endcode + * + * @DataType( + * id = "entity_revision_reference", + * label = @Translation("Entity reference revisions"), + * definition_class = "\Drupal\Core\TypedData\DataReferenceDefinition" + * ) + */ +class EntityReferenceRevisions extends EntityReference { + + /** + * The entity revision ID. + * + * @var integer|string + */ + protected $revision_id; + + /** + * The entity ID. + * + * @var integer|string + */ + protected $id; + + /** + * Returns the definition of the referenced entity. + * + * @return \Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface + * The reference target's definition. + */ + public function getTargetDefinition() { + return $this->definition->getTargetDefinition(); + } + + /** + * Checks whether the target entity has not been saved yet. + * + * @return bool + * TRUE if the entity is new, FALSE otherwise. + */ + public function isTargetNew() { + // If only an ID is given, the reference cannot be a new entity. + return !isset($this->id) && isset($this->target) && $this->target->getValue()->isNew(); + } + + /** + * {@inheritdoc} + */ + public function getTarget() { + if (!isset($this->target) && isset($this->revision_id)) { + $storage = \Drupal::entityTypeManager()->getStorage($this->getTargetDefinition()->getEntityTypeId()); + // By default always load the default revision, so caches get used. + $entity = $storage->load($this->id); + if ($entity !== NULL && $entity->getRevisionId() != $this->revision_id) { + // A non-default revision is a referenced, so load this one. + $entity = $storage->loadRevision($this->revision_id); + } + $this->target = isset($entity) ? $entity->getTypedData() : NULL; + } + return $this->target; + } + + /** + * {@inheritdoc} + */ + public function getTargetIdentifier() { + if (isset($this->id)) { + return $this->id; + } + elseif ($entity = $this->getValue()) { + return $entity->id(); + } + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + unset($this->target); + unset($this->id); + unset($this->revision_id); + + // Both the entity ID and the entity object may be passed as value. The + // reference may also be unset by passing NULL as value. + if (!isset($value)) { + $this->target = NULL; + } + elseif (is_object($value) && $value instanceof EntityInterface) { + $this->target = $value->getTypedData(); + } + elseif (!is_scalar($value['target_id']) || !is_scalar($value['target_revision_id']) || $this->getTargetDefinition()->getEntityTypeId() === NULL) { + throw new \InvalidArgumentException('Value is not a valid entity.'); + } + else { + $this->id = $value['target_id']; + $this->revision_id = $value['target_revision_id']; + } + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php new file mode 100644 index 000000000..461fd539e --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php @@ -0,0 +1,27 @@ +entityDefinitions as $entityType => $entityInfo) { + if ($entityInfo->getKey('revision')) { + $this->derivatives[$entityType] = [ + 'id' => "entity_reference_revisions:$entityType", + 'class' => EntityReferenceRevisions::class, + 'requirements_met' => 1, + 'provider' => $entityInfo->getProvider(), + ]; + } + } + return $this->derivatives; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php new file mode 100644 index 000000000..5d23ec47e --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php @@ -0,0 +1,164 @@ +loggerFactory = $logger_factory; + $this->entityDisplayRepository = $entity_display_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('logger.factory'), + $container->get('entity_display.repository') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return array( + 'view_mode' => 'default', + 'link' => FALSE, + ) + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $elements['view_mode'] = array( + '#type' => 'select', + '#options' => $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')), + '#title' => $this->t('View mode'), + '#default_value' => $this->getSetting('view_mode'), + '#required' => TRUE, + ); + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = array(); + + $view_modes = $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')); + $view_mode = $this->getSetting('view_mode'); + $summary[] = $this->t('Rendered as @mode', array('@mode' => isset($view_modes[$view_mode]) ? $view_modes[$view_mode] : $view_mode)); + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $view_mode = $this->getSetting('view_mode'); + $elements = array(); + + foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) { + // Protect ourselves from recursive rendering. + static $depth = 0; + $depth++; + if ($depth > 20) { + $this->loggerFactory->get('entity')->error('Recursive rendering detected when rendering entity @entity_type @entity_id. Aborting rendering.', array('@entity_type' => $entity->getEntityTypeId(), '@entity_id' => $entity->id())); + return $elements; + } + $view_builder = \Drupal::entityTypeManager()->getViewBuilder($entity->getEntityTypeId()); + $elements[$delta] = $view_builder->view($entity, $view_mode, $entity->language()->getId()); + + // Add a resource attribute to set the mapping property's value to the + // entity's url. Since we don't know what the markup of the entity will + // be, we shouldn't rely on it for structured data such as RDFa. + if (!empty($items[$delta]->_attributes) && !$entity->isNew() && $entity->hasLinkTemplate('canonical')) { + $items[$delta]->_attributes += array('resource' => $entity->toUrl()->toString()); + } + $depth = 0; + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + // This formatter is only available for entity types that have a view + // builder. + $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); + return \Drupal::entityTypeManager()->getDefinition($target_type)->hasViewBuilderClass(); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php new file mode 100644 index 000000000..e098f5280 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php @@ -0,0 +1,30 @@ +entity) { + $item->_loaded = TRUE; + } + } + } + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php new file mode 100644 index 000000000..24b322438 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php @@ -0,0 +1,512 @@ +getDefinitions(); + $options = array(); + foreach ($entity_types as $entity_type) { + if ($entity_type->isRevisionable()) { + $options[$entity_type->id()] = $entity_type->getLabel(); + } + } + + $element['target_type'] = array( + '#type' => 'select', + '#title' => $this->t('Type of item to reference'), + '#options' => $options, + '#default_value' => $this->getSetting('target_type'), + '#required' => TRUE, + '#disabled' => $has_data, + '#size' => 1, + ); + + return $element; + } + + /** + * {@inheritdoc} + */ + public static function getPreconfiguredOptions() { + $options = array(); + + // Add all the commonly referenced entity types as distinct pre-configured + // options. + $entity_types = \Drupal::entityTypeManager()->getDefinitions(); + $common_references = array_filter($entity_types, function (EntityTypeInterface $entity_type) { + return $entity_type->get('common_reference_revisions_target') && $entity_type->isRevisionable(); + }); + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + foreach ($common_references as $entity_type) { + + $options[$entity_type->id()] = [ + 'label' => $entity_type->getLabel(), + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $entity_type->id(), + ] + ] + ]; + $default_reference_settings = $entity_type->get('default_reference_revision_settings'); + if (is_array($default_reference_settings)) { + $options[$entity_type->id()] = array_merge($options[$entity_type->id()], $default_reference_settings); + } + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $settings = $field_definition->getSettings(); + $target_type_info = \Drupal::entityTypeManager()->getDefinition($settings['target_type']); + + $properties = parent::propertyDefinitions($field_definition); + + if ($target_type_info->getKey('revision')) { + $target_revision_id_definition = DataReferenceTargetDefinition::create('integer') + ->setLabel(t('@label revision ID', array('@label' => $target_type_info->getLabel()))) + ->setSetting('unsigned', TRUE); + + $target_revision_id_definition->setRequired(TRUE); + $properties['target_revision_id'] = $target_revision_id_definition; + } + + $properties['entity'] = DataReferenceDefinition::create('entity_revision') + ->setLabel($target_type_info->getLabel()) + ->setDescription(t('The referenced entity revision')) + // The entity object is computed out of the entity ID. + ->setComputed(TRUE) + ->setReadOnly(FALSE) + ->setTargetDefinition(EntityDataDefinition::create($settings['target_type'])); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $target_type = $field_definition->getSetting('target_type'); + $target_type_info = \Drupal::entityTypeManager()->getDefinition($target_type); + + $schema = parent::schema($field_definition); + + if ($target_type_info->getKey('revision')) { + $schema['columns']['target_revision_id'] = array( + 'description' => 'The revision ID of the target entity.', + 'type' => 'int', + 'unsigned' => TRUE, + ); + $schema['indexes']['target_revision_id'] = array('target_revision_id'); + } + + return $schema; + } + + /** + * {@inheritdoc} + */ + public function setValue($values, $notify = TRUE) { + if (isset($values) && !is_array($values)) { + // If either a scalar or an object was passed as the value for the item, + // assign it to the 'entity' property since that works for both cases. + $this->set('entity', $values, $notify); + } + else { + parent::setValue($values, FALSE); + // Support setting the field item with only one property, but make sure + // values stay in sync if only property is passed. + // NULL is a valid value, so we use array_key_exists(). + if (is_array($values) && array_key_exists('target_id', $values) && !isset($values['entity'])) { + $this->onChange('target_id', FALSE); + } + elseif (is_array($values) && array_key_exists('target_revision_id', $values) && !isset($values['entity'])) { + $this->onChange('target_revision_id', FALSE); + } + elseif (is_array($values) && !array_key_exists('target_id', $values) && !array_key_exists('target_revision_id', $values) && isset($values['entity'])) { + $this->onChange('entity', FALSE); + } + elseif (is_array($values) && array_key_exists('target_id', $values) && isset($values['entity'])) { + // If both properties are passed, verify the passed values match. The + // only exception we allow is when we have a new entity: in this case + // its actual id and target_id will be different, due to the new entity + // marker. + $entity_id = $this->get('entity')->getTargetIdentifier(); + // If the entity has been saved and we're trying to set both the + // target_id and the entity values with a non-null target ID, then the + // value for target_id should match the ID of the entity value. + if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id != $values['target_id'])) { + throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.'); + } + } + // Notify the parent if necessary. + if ($notify && $this->getParent()) { + $this->getParent()->onChange($this->getName()); + } + } + + } + + /** + * {@inheritdoc} + */ + public function getValue() { + $values = parent::getValue(); + if ($this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave()) { + $values['entity'] = $this->entity; + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function onChange($property_name, $notify = TRUE) { + // Make sure that the target ID and the target property stay in sync. + if ($property_name == 'entity') { + $property = $this->get('entity'); + $target_id = $property->isTargetNew() ? NULL : $property->getTargetIdentifier(); + $this->writePropertyValue('target_id', $target_id); + $this->writePropertyValue('target_revision_id', $property->getValue()->getRevisionId()); + } + elseif ($property_name == 'target_id' && $this->target_id != NULL && $this->target_revision_id) { + $this->writePropertyValue('entity', array( + 'target_id' => $this->target_id, + 'target_revision_id' => $this->target_revision_id, + )); + } + elseif ($property_name == 'target_revision_id' && $this->target_revision_id && $this->target_id) { + $this->writePropertyValue('entity', array( + 'target_id' => $this->target_id, + 'target_revision_id' => $this->target_revision_id, + )); + } + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + // Avoid loading the entity by first checking the 'target_id'. + if ($this->target_id !== NULL && $this->target_revision_id !== NULL) { + return FALSE; + } + if ($this->entity && $this->entity instanceof EntityInterface) { + return FALSE; + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function preSave() { + $has_new = $this->hasNewEntity(); + + // If it is a new entity, parent will save it. + parent::preSave(); + + $is_affected = TRUE; + if (!$has_new) { + // Create a new revision if it is a composite entity in a host with a new + // revision. + + $host = $this->getEntity(); + $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave(); + + // The item is considered to be affected if the field is either + // untranslatable or there are translation changes. This ensures that for + // translatable fields, a new revision of the referenced entity is only + // created for the affected translations and that the revision ID does not + // change on the unaffected translations. In turn, the host entity is not + // marked as affected for these translations. + $is_affected = !$this->getFieldDefinition()->isTranslatable() || ($host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges()); + if ($is_affected && !$host->isNew() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { + if ($host->isNewRevision()) { + $this->entity->setNewRevision(); + $needs_save = TRUE; + } + // Additionally ensure that the default revision state is kept in sync. + if ($this->entity && $host->isDefaultRevision() != $this->entity->isDefaultRevision()) { + $this->entity->isDefaultRevision($host->isDefaultRevision()); + $needs_save = TRUE; + } + } + if ($needs_save) { + $this->entity->save(); + } + } + if ($this->entity && $is_affected) { + $this->target_revision_id = $this->entity->getRevisionId(); + } + } + + /** + * {@inheritdoc} + */ + public function postSave($update) { + parent::postSave($update); + + $needs_save = FALSE; + // If any of entity, parent type or parent id is missing then return. + if (!$this->entity || !$this->entity->getEntityType()->get('entity_revision_parent_type_field') || !$this->entity->getEntityType()->get('entity_revision_parent_id_field')) { + return; + } + + $entity = $this->entity; + $parent_entity = $this->getEntity(); + + // If the entity has a parent field name get the key. + if ($entity->getEntityType()->get('entity_revision_parent_field_name_field')) { + $parent_field_name = $entity->getEntityType()->get('entity_revision_parent_field_name_field'); + + // If parent field name has changed then set it. + if ($entity->get($parent_field_name)->value != $this->getFieldDefinition()->getName()) { + $entity->set($parent_field_name, $this->getFieldDefinition()->getName()); + $needs_save = TRUE; + } + } + + // Keep in sync the translation languages between the parent and the child. + // For non translatable fields we have to do this in ::preSave but for + // translatable fields we have all the information we need in ::delete. + if (isset($parent_entity->original) && !$this->getFieldDefinition()->isTranslatable()) { + $langcodes = array_keys($parent_entity->getTranslationLanguages()); + $original_langcodes = array_keys($parent_entity->original->getTranslationLanguages()); + if ($removed_langcodes = array_diff($original_langcodes, $langcodes)) { + foreach ($removed_langcodes as $removed_langcode) { + if ($entity->hasTranslation($removed_langcode) && $entity->getUntranslated()->language()->getId() != $removed_langcode) { + $entity->removeTranslation($removed_langcode); + } + } + $needs_save = TRUE; + } + } + + $parent_type = $entity->getEntityType()->get('entity_revision_parent_type_field'); + $parent_id = $entity->getEntityType()->get('entity_revision_parent_id_field'); + + // If the parent type has changed then set it. + if ($entity->get($parent_type)->value != $parent_entity->getEntityTypeId()) { + $entity->set($parent_type, $parent_entity->getEntityTypeId()); + $needs_save = TRUE; + } + // If the parent id has changed then set it. + if ($entity->get($parent_id)->value != $parent_entity->id()) { + $entity->set($parent_id, $parent_entity->id()); + $needs_save = TRUE; + } + + if ($needs_save) { + // Check if any of the keys has changed, save it, do not create a new + // revision. + $entity->setNewRevision(FALSE); + $entity->save(); + } + } + + /** + * {@inheritdoc} + */ + public function deleteRevision() { + $child = $this->entity; + // Return early, and do not delete the child revision, when the child + // revision is either: + // 1: Missing. + // 2: A default revision. + if (!$child || $child->isDefaultRevision()) { + return; + } + + $host = $this->getEntity(); + $field_name = $this->getFieldDefinition()->getName() . '.target_revision_id'; + $all_revisions = \Drupal::entityQuery($host->getEntityTypeId()) + ->condition($field_name, $child->getRevisionId()) + ->allRevisions() + ->accessCheck(FALSE) + ->execute(); + + if (count($all_revisions) > 1) { + // Do not delete if there is more than one usage of this revision. + return; + } + + \Drupal::entityTypeManager()->getStorage($child->getEntityTypeId())->deleteRevision($child->getRevisionId()); + } + + /** + * {@inheritdoc} + */ + public function delete() { + parent::delete(); + + if ($this->entity && $this->entity->getEntityType()->get('entity_revision_parent_type_field') && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { + // Only delete composite entities if the host field is not translatable. + if (!$this->getFieldDefinition()->isTranslatable()) { + $this->entity->delete(); + } + } + } + + /** + * {@inheritdoc} + */ + public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) { + $changed = FALSE; + $entity_manager = \Drupal::entityManager(); + $target_entity_type = $entity_manager->getDefinition($field_definition->getFieldStorageDefinition() + ->getSetting('target_type')); + $handler_settings = $field_definition->getSetting('handler_settings'); + + // Update the 'target_bundles' handler setting if a bundle config dependency + // has been removed. + if (!empty($handler_settings['target_bundles'])) { + if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) { + if ($storage = $entity_manager->getStorage($bundle_entity_type_id)) { + foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) { + if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) { + unset($handler_settings['target_bundles'][$bundle->id()]); + $changed = TRUE; + + // In case we deleted the only target bundle allowed by the field + // we can log a message because the behaviour of the field will + // have changed. + if ($handler_settings['target_bundles'] === []) { + \Drupal::logger('entity_reference_revisions') + ->notice('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference revisions field (entity_type: %entity_type, bundle: %bundle) no longer specifies a specific target bundle. The field will now accept any bundle and may need to be adjusted.', [ + '%target_bundle' => $bundle->label(), + '%target_entity_type' => $bundle->getEntityType() + ->getBundleOf(), + '%field_name' => $field_definition->getName(), + '%entity_type' => $field_definition->getTargetEntityTypeId(), + '%bundle' => $field_definition->getTargetBundle() + ]); + } + } + } + } + } + } + + if ($changed) { + $field_definition->setSetting('handler_settings', $handler_settings); + } + + return $changed; + } + + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection'); + $entity_manager = \Drupal::entityTypeManager(); + + // Bail if there are no referenceable entities. + if (!$selection_manager->getSelectionHandler($field_definition)->getReferenceableEntities()) { + return; + } + + // ERR field values are never cross referenced so we need to generate new + // target entities. First, find the target entity type. + $target_type_id = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); + $target_type = $entity_manager->getDefinition($target_type_id); + $handler_settings = $field_definition->getSetting('handler_settings'); + + // Determine referenceable bundles. + $bundle_manager = \Drupal::service('entity_type.bundle.info'); + if (isset($handler_settings['target_bundles']) && is_array($handler_settings['target_bundles'])) { + $bundles = $handler_settings['target_bundles']; + } + else { + $bundles = $bundle_manager->getBundleInfo($target_type_id); + } + $bundle = array_rand($bundles); + + $label = NULL; + if ($label_key = $target_type->getKey('label')) { + $random = new Random(); + // @TODO set the length somehow less arbitrary. + $label = $random->word(mt_rand(1, 10)); + } + + // Create entity stub. + $entity = $selection_manager->getSelectionHandler($field_definition)->createNewEntity($target_type_id, $bundle, $label, 0); + + // Populate entity values and save. + $instances = $entity_manager + ->getStorage('field_config') + ->loadByProperties([ + 'entity_type' => $target_type_id, + 'bundle' => $bundle, + ]); + + foreach ($instances as $instance) { + $field_storage = $instance->getFieldStorageDefinition(); + $max = $cardinality = $field_storage->getCardinality(); + if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { + // Just an arbitrary number for 'unlimited' + $max = rand(1, 5); + } + $field_name = $field_storage->getName(); + $entity->{$field_name}->generateSampleItems($max); + } + + $entity->save(); + + return [ + 'target_id' => $entity->id(), + 'target_revision_id' => $entity->getRevisionId(), + ]; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php new file mode 100644 index 000000000..faad139ab --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php @@ -0,0 +1,44 @@ +fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'); + foreach ($values as $key => $value) { + if($value['target_id']) { + $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($value['target_id']); + // Add the current revision ID. + $values[$key]['target_revision_id'] = $entity->getRevisionId(); + } + // The entity_autocomplete form element returns an array when an entity + // was "autocreated", so we need to move it up a level. + if (is_array($value['target_id'])) { + unset($values[$key]['target_id']); + $values[$key] += $value['target_id']; + } + } + return $values; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php new file mode 100644 index 000000000..0df05077a --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php @@ -0,0 +1,59 @@ + $field_item) { + if (!$field_item->isEmpty() && $field_item->entity) { + $parsed_text = $this->entityParser->parseEntity($field_item->entity); + if (is_array($parsed_text)) { + foreach ($parsed_text as $field_id => $field) { + foreach ($field as $id => $text) { + $result_text[$item_counter + $id] = $text; + } + $item_counter = $item_counter + $id + 1; + } + } + } + } + return $result_text; + } + + /** + * {@inheritdoc} + */ + public function getEntitiesToDiff(FieldItemListInterface $field_items) { + /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item */ + $entities = []; + foreach ($field_items as $field_key => $field_item) { + if (!$field_item->isEmpty() && $field_item->entity) { + $entities[$field_key] = $field_item->entity; + } + } + return $entities; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php new file mode 100644 index 000000000..75644b2f6 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php @@ -0,0 +1,219 @@ +setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = NestedArray::mergeDeep( + $this->defaultConfiguration(), + $configuration + ); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'new_revisions' => FALSE, + ]; + } + + /** + * {@inheritdoc} + */ + protected static function getEntityTypeId($pluginId) { + // Remove "entity_reference_revisions:". + // Ideally, we would call getDerivativeId(), but since this is static + // that is not possible so we follow the same pattern as core. + return substr($pluginId, 27); + } + + /** + * {@inheritdoc} + */ + protected function save(ContentEntityInterface $entity, array $oldDestinationIdValues = []) { + $entity->save(); + + return [ + $this->getKey('id') => $entity->id(), + $this->getKey('revision') => $entity->getRevisionId(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + if ($revision_key = $this->getKey('revision')) { + $id_key = $this->getKey('id'); + $ids[$id_key]['type'] = 'integer'; + + // TODO: Improve after https://www.drupal.org/node/2783715 is finished. + $ids[$revision_key]['type'] = 'integer'; + + if ($this->isTranslationDestination()) { + if ($revision_key = $this->getKey('langcode')) { + $ids[$revision_key]['type'] = 'string'; + } + else { + throw new MigrateException('This entity type does not support translation.'); + } + } + + return $ids; + } + throw new MigrateException('This entity type does not support revisions.'); + } + + /** + * {@inheritdoc} + */ + protected function getEntity(Row $row, array $oldDestinationIdValues) { + $entity_id = $oldDestinationIdValues ? + array_shift($oldDestinationIdValues) : + $this->getEntityId($row); + $revision_id = $oldDestinationIdValues ? + array_pop($oldDestinationIdValues) : + $row->getDestinationProperty($this->getKey('revision')); + + // If a specific revision_id is supplied and exists, assert the entity_id + // matches (if supplied), and update the revision. + /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityInterface $entity */ + if (!empty($revision_id) && ($entity = $this->storage->loadRevision($revision_id))) { + if (!empty($entity_id) && ($entity->id() != $entity_id)) { + throw new MigrateException("The revision_id exists for this entity type, but does not belong to the given entity id"); + } + $entity = $this->updateEntity($entity, $row) ?: $entity; + } + // If there is no revision_id supplied, but there is an entity_id + // supplied that exists, update it. + elseif (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) { + // If so configured, create a new revision while updating. + if ($this->getConfiguration()['new_revisions']) { + $entity->setNewRevision(TRUE); + } + $entity = $this->updateEntity($entity, $row) ?: $entity; + } + + // Otherwise, create a new (possibly stub) entity. + else { + // Attempt to ensure we always have a bundle. + if ($bundle = $this->getBundle($row)) { + $row->setDestinationProperty($this->getKey('bundle'), $bundle); + } + + // Stubs might need some required fields filled in. + if ($row->isStub()) { + $this->processStubRow($row); + } + $entity = $this->storage->create($row->getDestination()) + ->enforceIsNew(TRUE); + $entity->setNewRevision(TRUE); + } + $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE; + return $entity; + } + + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifiers) { + if ($this->isTranslationDestination()) { + $this->rollbackTranslation($destination_identifiers); + } + else { + $this->rollbackNonTranslation($destination_identifiers); + } + } + + /** + * Rollback translation destinations. + * + * @param array $destination_identifiers + * The IDs of the destination object to delete. + */ + protected function rollbackTranslation(array $destination_identifiers) { + $entity = $this->storage->loadRevision(array_pop($destination_identifiers)); + if ($entity && $entity instanceof TranslatableInterface) { + if ($key = $this->getKey('langcode')) { + if (isset($destination_identifier[$key])) { + $langcode = $destination_identifier[$key]; + if ($entity->hasTranslation($langcode)) { + // Make sure we don't remove the default translation. + $translation = $entity->getTranslation($langcode); + if (!$translation->isDefaultTranslation()) { + $entity->removeTranslation($langcode); + $entity->save(); + } + } + } + } + } + } + + /** + * Rollback non-translation destinations. + * + * @param array $destination_identifiers + * The IDs of the destination object to delete. + */ + protected function rollbackNonTranslation(array $destination_identifiers) { + $revision_id = array_pop($destination_identifiers); + $entity = $this->storage->loadRevision($revision_id); + if ($entity) { + if ($entity->isDefaultRevision()) { + $entity->delete(); + } + else { + $this->storage->deleteRevision($revision_id); + } + } + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php new file mode 100644 index 000000000..3c738564d --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php @@ -0,0 +1,176 @@ + 'entity_reference_revisions'); + $options['defaults']['default']['style'] = FALSE; + $options['row']['contains']['type'] = array('default' => 'entity_reference_revisions'); + $options['defaults']['default']['row'] = FALSE; + + // Make sure the query is not cached. + $options['defaults']['default']['cache'] = FALSE; + + // Set the display title to an empty string (not used in this display type). + $options['title']['default'] = ''; + $options['defaults']['default']['title'] = FALSE; + + return $options; + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::optionsSummary(). + * + * Disable 'cache' and 'title' so it won't be changed. + */ + public function optionsSummary(&$categories, &$options) { + parent::optionsSummary($categories, $options); + unset($options['query']); + unset($options['title']); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return 'entity_reference_revisions'; + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::execute(). + */ + public function execute() { + return $this->view->render($this->display['id']); + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::render(). + */ + public function render() { + if (!empty($this->view->result) && $this->view->style_plugin->evenEmpty()) { + return $this->view->style_plugin->render($this->view->result); + } + return ''; + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::usesExposed(). + */ + public function usesExposed() { + return FALSE; + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::query(). + */ + public function query() { + if (!empty($this->view->live_preview)) { + return; + } + + // Make sure the id field is included in the results. + $id_field = $this->view->storage->get('base_field'); + $this->id_field_alias = $this->view->query->addField($this->view->storage->get('base_table'), $id_field); + + $options = $this->getOption('entity_reference_revisions_options'); + + // Restrict the autocomplete options based on what's been typed already. + if (isset($options['match'])) { + $style_options = $this->getOption('style'); + $value = db_like($options['match']) . '%'; + if ($options['match_operator'] != 'STARTS_WITH') { + $value = '%' . $value; + } + + // Multiple search fields are OR'd together. + $conditions = db_or(); + + // Build the condition using the selected search fields. + foreach ($style_options['options']['search_fields'] as $field_alias) { + if (!empty($field_alias)) { + // Get the table and field names for the checked field. + $field = $this->view->query->fields[$this->view->field[$field_alias]->field_alias]; + // Add an OR condition for the field. + $conditions->condition($field['table'] . '.' . $field['field'], $value, 'LIKE'); + } + } + + $this->view->query->addWhere(0, $conditions); + } + + // Add an IN condition for validation. + if (!empty($options['ids'])) { + $this->view->query->addWhere(0, $id_field, $options['ids']); + } + + $this->view->setItemsPerPage($options['limit']); + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::validate(). + */ + public function validate() { + $errors = parent::validate(); + // Verify that search fields are set up. + $style = $this->getOption('style'); + if (!isset($style['options']['search_fields'])) { + $errors[] = $this->t('Display "@display" needs a selected search fields to work properly. See the settings for the Entity Reference Revisions list format.', array('@display' => $this->display['display_title'])); + } + else { + // Verify that the search fields used actually exist. + $fields = array_keys($this->handlers['field']); + foreach ($style['options']['search_fields'] as $field_alias => $enabled) { + if ($enabled && !in_array($field_alias, $fields)) { + $errors[] = $this->t('Display "@display" uses field %field as search field, but the field is no longer present. See the settings for the Entity Reference Revisions list format.', array('@display' => $this->display['display_title'], '%field' => $field_alias)); + } + } + } + return $errors; + } +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php new file mode 100644 index 000000000..fe25de202 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php @@ -0,0 +1,57 @@ + '-'); + + return $options; + } + + /** + * Overrides \Drupal\views\Plugin\views\row\Fields::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + // Expand the description of the 'Inline field' checkboxes. + $form['inline']['#description'] .= '
' . $this->t("Note: In 'Entity Reference' displays, all fields will be displayed inline unless an explicit selection of inline fields is made here." ); + } + + /** + * {@inheritdoc} + */ + public function preRender($row) { + // Force all fields to be inline by default. + if (empty($this->options['inline'])) { + $fields = $this->view->getHandlers('field', $this->displayHandler->display['id']); + $names = array_keys($fields); + $this->options['inline'] = array_combine($names, $names); + } + + return parent::preRender($row); + } +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php new file mode 100644 index 000000000..376491ee7 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php @@ -0,0 +1,103 @@ + NULL); + + return $options; + } + + /** + * Overrides \Drupal\views\Plugin\views\style\StylePluginBase\StylePluginBase::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $options = $this->displayHandler->getFieldLabels(TRUE); + $form['search_fields'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Search fields'), + '#options' => $options, + '#required' => TRUE, + '#default_value' => $this->options['search_fields'], + '#description' => $this->t('Select the field(s) that will be searched when using the autocomplete widget.'), + '#weight' => -3, + ); + } + + /** + * Overrides \Drupal\views\Plugin\views\style\StylePluginBase\StylePluginBase::render(). + */ + public function render() { + if (!empty($this->view->live_preview)) { + return parent::render(); + } + + // Group the rows according to the grouping field, if specified. + $sets = $this->renderGrouping($this->view->result, $this->options['grouping']); + + // Grab the alias of the 'id' field added by + // entity_reference_plugin_display. + $id_field_alias = $this->view->storage->get('base_field'); + + // @todo We don't display grouping info for now. Could be useful for select + // widget, though. + $results = array(); + foreach ($sets as $records) { + foreach ($records as $values) { + // Sanitize HTML, remove line breaks and extra whitespace. + $output = $this->view->rowPlugin->render($values); + $output = \Drupal::service('renderer')->render($output); + $results[$values->{$id_field_alias}] = Xss::filterAdmin(preg_replace('/\s\s+/', ' ', str_replace("\n", '', $output))); + } + } + return $results; + } + + /** + * {@inheritdoc} + */ + public function evenEmpty() { + return TRUE; + } +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php new file mode 100644 index 000000000..cc7caeddb --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php @@ -0,0 +1,184 @@ +drupalCreateContentType(array('type' => 'entity_revisions', 'name' => 'Entity revisions')); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + // Place the breadcrumb, tested in fieldUIAddNewField(). + $this->drupalPlaceBlock('system_breadcrumb_block'); + $admin_user = $this->drupalCreateUser(array( + 'administer site configuration', + 'administer nodes', + 'create article content', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer node form display', + 'edit any article content', + )); + $this->drupalLogin($admin_user); + } + + /** + * Tests the entity reference revisions configuration. + */ + public function testEntityReferenceRevisions() { + // Create a test target node used as entity reference by another test node. + $node_target = Node::create([ + 'title' => 'Target node', + 'type' => 'article', + 'body' => 'Target body text', + 'uuid' => '2d04c2b4-9c3d-4fa6-869e-ecb6fa5c9410', + ]); + $node_target->save(); + + // Add an entity reference revisions field to entity_revisions content type + // with $node_target as default value. + $storage_edit = ['settings[target_type]' => 'node', 'cardinality' => '-1']; + $field_edit = [ + 'settings[handler_settings][target_bundles][article]' => TRUE, + 'default_value_input[field_entity_reference_revisions][0][target_id]' => $node_target->label() . ' (' . $node_target->id() . ')', + ]; + static::fieldUIAddNewField('admin/structure/types/manage/entity_revisions', 'entity_reference_revisions', 'Entity reference revisions', 'entity_reference_revisions', $storage_edit, $field_edit); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + $this->assertText('Saved Entity reference revisions configuration.'); + + // Resave the target node, so that the default revision is not the one we + // want to use. + $revision_id = $node_target->getRevisionId(); + $node_target_after = Node::load($node_target->id()); + $node_target_after->setNewRevision(); + $node_target_after->save(); + $this->assertTrue($node_target_after->getRevisionId() != $revision_id); + + // Create an article. + $title = $this->randomMachineName(); + $edit = array( + 'title[0][value]' => $title, + 'body[0][value]' => 'Revision 1', + ); + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); + $this->assertText($title); + $this->assertText('Revision 1'); + $node = $this->drupalGetNodeByTitle($title); + + // Check if when creating an entity revisions content the default entity + // reference is set, add also the above article as a new reference. + $this->drupalGet('node/add/entity_revisions'); + $this->assertFieldByName('field_entity_reference_revisions[0][target_id]', $node_target->label() . ' (' . $node_target->id() . ')'); + $edit = [ + 'title[0][value]' => 'Entity reference revision content', + 'field_entity_reference_revisions[1][target_id]' => $node->label() . ' (' . $node->id() . ')', + ]; + $this->drupalPostNodeForm(NULL, $edit, t('Save and publish')); + $this->assertLinkByHref('node/' . $node_target->id()); + $this->assertText('Entity revisions Entity reference revision content has been created.'); + $this->assertText('Entity reference revision content'); + $this->assertText($title); + $this->assertText('Revision 1'); + + // Create 2nd revision of the article. + $edit = array( + 'body[0][value]' => 'Revision 2', + 'revision' => TRUE, + ); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $this->assertText($title); + $this->assertText('Revision 2'); + + // View the Entity reference content and make sure it still has revision 1. + $node = $this->drupalGetNodeByTitle('Entity reference revision content'); + $this->drupalGet('node/' . $node->id()); + $this->assertText($title); + $this->assertText('Revision 1'); + $this->assertNoText('Revision 2'); + + // Make sure the non-revisionable entities are not selectable as referenced + // entities. + $edit = array( + 'new_storage_type' => 'entity_reference_revisions', + 'label' => 'Entity reference revisions field', + 'field_name' => 'entity_ref_revisions_field', + ); + $this->drupalPostForm('admin/structure/types/manage/entity_revisions/fields/add-field', $edit, t('Save and continue')); + $this->assertNoOption('edit-settings-target-type', 'user'); + $this->assertOption('edit-settings-target-type', 'node'); + + // Check ERR default value and property definitions label are set properly. + $field_definition = $node->getFieldDefinition('field_entity_reference_revisions'); + $default_value = $field_definition->toArray()['default_value']; + $this->assertEqual($default_value[0]['target_uuid'], $node_target->uuid()); + $this->assertEqual($default_value[0]['target_revision_id'], $revision_id); + $properties = $field_definition->getFieldStorageDefinition()->getPropertyDefinitions(); + $this->assertEqual((string) $properties['target_revision_id']->getLabel(), 'Content revision ID'); + $this->assertEqual((string) $properties['target_id']->getLabel(), 'Content ID'); + $this->assertEqual((string) $properties['entity']->getLabel(), 'Content'); + } + + /** + * Tests target bundle settings for an entity reference revisions field. + */ + public function testMultipleTargetBundles() { + // Create a couple of content types for the ERR field to point to. + $target_types = []; + for ($i = 0; $i < 2; $i++) { + $target_types[$i] = $this->drupalCreateContentType([ + 'type' => strtolower($this->randomMachineName()), + 'name' => 'Test type ' . $i + ]); + } + + // Create a new field that can point to either target content type. + $node_type_path = 'admin/structure/types/manage/entity_revisions'; + + // Generate a random field name, must be only lowercase characters. + $field_name = strtolower($this->randomMachineName()); + + $field_edit = []; + $storage_edit = ['settings[target_type]' => 'node', 'cardinality' => '-1']; + $field_edit['settings[handler_settings][target_bundles][' . $target_types[0]->id() . ']'] = TRUE; + $field_edit['settings[handler_settings][target_bundles][' . $target_types[1]->id() . ']'] = TRUE; + + $this->fieldUIAddNewField($node_type_path, $field_name, 'Entity reference revisions', 'entity_reference_revisions', $storage_edit, $field_edit); + + // Deleting one of these content bundles at this point should only delete + // that bundle's body field. Test that there is no second field that will + // be deleted. + $this->drupalGet('/admin/structure/types/manage/' . $target_types[0]->id() . '/delete'); + $this->assertNoFieldByXPath('(//details[@id="edit-entity-deletes"]//ul[@data-drupal-selector="edit-field-config"]/li)[2]'); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php new file mode 100644 index 000000000..a80c615e5 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php @@ -0,0 +1,147 @@ +drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + // Place the breadcrumb, tested in fieldUIAddNewField(). + $this->drupalPlaceBlock('system_breadcrumb_block'); + } + + /** + * Test for autocomplete processing. + * + * Tests that processing does not crash when the entity types of the + * referenced entity and of the entity the field is attached to are different. + */ + public function testEntityReferenceRevisionsAutocompleteProcessing() { + $admin_user = $this->drupalCreateUser(array( + 'administer site configuration', + 'administer nodes', + 'administer blocks', + 'create article content', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer node form display', + 'edit any article content', + )); + $this->drupalLogin($admin_user); + + // Create a custom block content bundle. + $this->createBlockContentType(array('type' => 'customblockcontent', 'name' => 'Custom Block Content')); + + // Create entity reference revisions field attached to article. + static::fieldUIAddNewField( + 'admin/structure/types/manage/article', + 'entity_reference_revisions', + 'Entity reference revisions', + 'entity_reference_revisions', + array('settings[target_type]' => 'block_content', 'cardinality' => '-1'), + array('settings[handler_settings][target_bundles][customblockcontent]' => TRUE) + ); + + // Create custom block. + $block_label = $this->randomMachineName(); + $block_content = $this->randomString(); + $edit = array( + 'info[0][value]' => $block_label, + 'body[0][value]' => $block_content, + ); + $this->drupalPostForm('block/add', $edit, t('Save')); + $block = $this->drupalGetBlockByInfo($block_label); + + // Create an article. + $title = $this->randomMachineName(); + $edit = array( + 'title[0][value]' => $title, + 'body[0][value]' => 'Revision 1', + 'field_entity_reference_revisions[0][target_id]' => $block_label . ' (' . $block->id() . ')', + ); + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); + $this->assertText($title); + $this->assertText(Html::escape($block_content)); + + // Check if the block content is not deleted since there is no composite + // relationship. + $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); + $node = Node::load($node->id()); + $node->delete(); + $this->assertNotNull(BlockContent::load($block->id())); + } + + /** + * Get a custom block from the database based on its title. + * + * @param $info + * A block title, usually generated by $this->randomMachineName(). + * @param $reset + * (optional) Whether to reset the entity cache. + * + * @return \Drupal\block\BlockInterface + * A block entity matching $info. + */ + function drupalGetBlockByInfo($info, $reset = FALSE) { + if ($reset) { + \Drupal::entityTypeManager()->getStorage('block_content')->resetCache(); + } + $blocks = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties(array('info' => $info)); + // Get the first block returned from the database. + $returned_block = reset($blocks); + return $returned_block; + } + + /** + * Create a block_content bundle. + * + * @param $parameters + * An assoc array with name (human readable) and type (bundle machine name) + * as keys. + */ + function createBlockContentType($parameters) { + $label = $parameters['name']; + $machine_name = $parameters['type']; + $edit = array( + 'label' => $label, + 'id' => $machine_name, + 'revision' => TRUE, + ); + $this->drupalPostForm('admin/structure/block/block-content/types/add', $edit, t('Save')); + $this->assertText($label); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php new file mode 100644 index 000000000..596d9416f --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php @@ -0,0 +1,35 @@ + 8.4 Save (and (un)publish) node button change. + * + * @see \Drupal\simpletest\WebTestBase::drupalPostForm + * @see https://www.drupal.org/node/2847274 + */ + protected function drupalPostNodeForm($path, $edit, $submit, array $options = [], array $headers = [], $form_html_id = NULL, $extra_post = NULL) { + $drupal_version = (float) substr(\Drupal::VERSION, 0, 3); + if ($drupal_version > 8.3) { + + switch ($submit) { + case t('Save and unpublish'): + $edit['status[value]'] = FALSE; + break; + + case t('Save and publish'): + $edit['status[value]'] = TRUE; + break; + } + + $submit = t('Save'); + } + parent::drupalPostForm($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php new file mode 100644 index 000000000..d4f236abe --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php @@ -0,0 +1,121 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + + // Disable visual inline diff. + $config = $this->config('diff.settings') + ->set('general_settings.layout_plugins.visual_inline.enabled', FALSE); + $config->save(); + + $admin_user = $this->drupalCreateUser([ + 'administer site configuration', + 'administer nodes', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer node form display', + 'view all revisions', + 'edit any article content', + ]); + $this->drupalLogin($admin_user); + $this->drupalPlaceBlock('system_breadcrumb_block'); + } + + /** + * Test for diff plugin of ERR. + * + * Tests that the diff is displayed when changes are made in an ERR field. + */ + public function testEntityReferenceRevisionsDiff() { + // Add an entity_reference_revisions field. + static::fieldUIAddNewField('admin/structure/types/manage/article', 'err_field', 'err_field', 'entity_reference_revisions', [ + 'settings[target_type]' => 'node', + 'cardinality' => '-1', + ], [ + 'settings[handler_settings][target_bundles][article]' => TRUE, + ]); + + // Create first referenced node. + $title_node_1 = 'referenced_node_1'; + $edit = [ + 'title[0][value]' => $title_node_1, + 'body[0][value]' => 'body_node_1', + ]; + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); + + // Create second referenced node. + $title_node_2 = 'referenced_node_2'; + $edit = [ + 'title[0][value]' => $title_node_2, + 'body[0][value]' => 'body_node_2', + ]; + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); + + // Create referencing node. + $title = 'referencing_node'; + $node = $this->drupalGetNodeByTitle($title_node_1); + $edit = [ + 'title[0][value]' => $title, + 'field_err_field[0][target_id]' => $title_node_1 . ' (' . $node->id() . ')', + ]; + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); + + // Check the plugin is set. + $this->drupalGet('admin/config/content/diff/fields'); + $this->drupalPostForm(NULL, ['fields[node.field_err_field][plugin][type]' => 'entity_reference_revisions_field_diff_builder'], t('Save')); + + // Update the referenced node of the err field and create a new revision. + $node = $this->drupalGetNodeByTitle($title); + $referenced_node_new = $this->drupalGetNodeByTitle($title_node_2); + $edit = [ + 'field_err_field[0][target_id]' => $title_node_2 . ' (' . $referenced_node_new->id() . ')', + 'revision' => TRUE, + ]; + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + + // Compare the revisions of the referencing node. + $this->drupalPostForm('node/' . $node->id() . '/revisions', [], t('Compare selected revisions')); + + // Assert the field changes. + $this->assertRaw('class="diff-context diff-deletedline">' . $title_node_1); + $this->assertRaw('class="diff-context diff-addedline">' . $title_node_2); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php new file mode 100644 index 000000000..ea5bcaf9e --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php @@ -0,0 +1,106 @@ +drupalCreateContentType(array('type' => 'entity_revisions', 'name' => 'Entity revisions')); + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + // Place the breadcrumb, tested in fieldUIAddNewField(). + $this->drupalPlaceBlock('system_breadcrumb_block'); + } + + /** + * Tests the entity reference revisions configuration. + */ + public function testEntityReferenceRevisions() { + $admin_user = $this->drupalCreateUser(array( + 'administer site configuration', + 'administer nodes', + 'create article content', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer node form display', + 'edit any article content', + )); + $this->drupalLogin($admin_user); + // Create entity reference revisions field. + static::fieldUIAddNewField('admin/structure/types/manage/entity_revisions', 'entity_reference_revisions', 'Entity reference revisions', 'entity_reference_revisions', array('settings[target_type]' => 'node', 'cardinality' => '-1'), array('settings[handler_settings][target_bundles][article]' => TRUE)); + $this->assertText('Saved Entity reference revisions configuration.'); + + // Create an article. + $title = $this->randomMachineName(); + $edit = array( + 'title[0][value]' => $title, + 'body[0][value]' => 'Revision 1', + ); + $this->drupalPostNodeForm('node/add/article', $edit, t('Save and publish')); + $this->assertText($title); + $this->assertText('Revision 1'); + $node = $this->drupalGetNodeByTitle($title); + + // Create entity revisions content that includes the above article. + $err_title = 'Entity reference revision content'; + $edit = array( + 'title[0][value]' => $err_title, + 'field_entity_reference_revisions[0][target_id]' => $node->label() . ' (' . $node->id() . ')', + ); + $this->drupalPostNodeForm('node/add/entity_revisions', $edit, t('Save and publish')); + $this->assertText('Entity revisions Entity reference revision content has been created.'); + $err_node = $this->drupalGetNodeByTitle($err_title); + + $this->assertText($err_title); + $this->assertText($title); + $this->assertText('Revision 1'); + + // Create 2nd revision of the article. + $edit = array( + 'body[0][value]' => 'Revision 2', + 'revision' => TRUE, + ); + $this->drupalPostNodeForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + $serializer = $this->container->get('serializer'); + $normalized = $serializer->normalize($err_node, 'hal_json'); + $request = \Drupal::request(); + $link_domain = $request->getSchemeAndHttpHost() . $request->getBasePath(); + $this->assertEqual($err_node->field_entity_reference_revisions->target_revision_id, $normalized['_embedded'][$link_domain . '/rest/relation/node/entity_revisions/field_entity_reference_revisions'][0]['target_revision_id']); + $new_err_node = $serializer->denormalize($normalized, Node::class, 'hal_json'); + $this->assertEqual($err_node->field_entity_reference_revisions->target_revision_id, $new_err_node->field_entity_reference_revisions->target_revision_id); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php b/web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php new file mode 100644 index 000000000..7ffac7d8f --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php @@ -0,0 +1,50 @@ +setEntityTypeId($parts[1]); + } + if (isset($parts[2])) { + $definition->setBundles(array($parts[2])); + } + return $definition; + } + + /** + * {@inheritdoc} + */ + public function getDataType() { + $type = 'entity_revision'; + if ($entity_type = $this->getEntityTypeId()) { + $type .= ':' . $entity_type; + // Append the bundle only if we know it for sure and it is not the default + // bundle. + if (($bundles = $this->getBundles()) && count($bundles) == 1) { + $bundle = reset($bundles); + if ($bundle != $entity_type) { + $type .= ':' . $bundle; + } + } + } + return $type; + } +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml new file mode 100644 index 000000000..2172d62f0 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml @@ -0,0 +1,14 @@ +name: 'ERR Composite relationship test' +type: module +description: 'Entity with parent type and ID.' +package: Testing +# core: 8.x + +dependencies: + - entity_reference_revisions + - entity_test +# Information added by Drupal.org packaging script on 2018-10-15 +version: '8.x-1.6' +core: '8.x' +project: 'entity_reference_revisions' +datestamp: 1539588784 diff --git a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml new file mode 100644 index 000000000..c9be8a03b --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml @@ -0,0 +1,2 @@ +administer entity_test composite relationship: + title: administer entity_test composite relationship diff --git a/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php new file mode 100644 index 000000000..c74765faf --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php @@ -0,0 +1,61 @@ +setLabel(t('Parent ID')) + ->setDescription(t('The ID of the parent entity of which this entity is referenced.')); + + $fields['parent_type'] = BaseFieldDefinition::create('string') + ->setLabel(t('Parent type')) + ->setDescription(t('The entity parent type to which this entity is referenced.')); + + $fields['parent_field_name'] = BaseFieldDefinition::create('string') + ->setLabel(t('Parent field name')) + ->setDescription(t('The entity parent field name to which this entity is referenced.')); + + return $fields; + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php new file mode 100644 index 000000000..1509e44b6 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php @@ -0,0 +1,423 @@ +installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => FALSE, + )); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + } + + /** + * Test for maintaining composite relationship. + * + * Tests that the referenced entity saves the parent type and id when saving. + */ + public function testEntityReferenceRevisionsCompositeRelationship() { + // Create the test composite entity. + $composite = EntityTestCompositeRelationship::create(array( + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + )); + $composite->save(); + + // Assert that there is only 1 revision of the composite entity. + $composite_revisions_count = \Drupal::entityQuery('entity_test_composite')->condition('uuid', $composite->uuid())->allRevisions()->count()->execute(); + $this->assertEquals(1, $composite_revisions_count); + + // Create a node with a reference to the test composite entity. + /** @var \Drupal\node\NodeInterface $node */ + $node = Node::create(array( + 'title' => $this->randomMachineName(), + 'type' => 'article', + )); + $node->save(); + $node->set('composite_reference', $composite); + $this->assertTrue($node->hasTranslationChanges()); + $node->save(); + + // Assert that there is only 1 revision when creating a node. + $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); + $this->assertEqual($node_revisions_count, 1); + // Assert there is no new composite revision after creating a host entity. + $composite_revisions_count = \Drupal::entityQuery('entity_test_composite')->condition('uuid', $composite->uuid())->allRevisions()->count()->execute(); + $this->assertEquals(1, $composite_revisions_count); + + // Verify the value of parent type and id after create a node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); + $this->assertEqual($composite->parent_id->value, $node->id()); + $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + // Create second revision of the node. + $original_composite_revision = $node->composite_reference[0]->target_revision_id; + $original_node_revision = $node->getRevisionId(); + $node->setTitle('2nd revision'); + $node->setNewRevision(); + $node->save(); + $node = node_load($node->id(), TRUE); + // Check the revision of the node. + $this->assertEqual('2nd revision', $node->getTitle(), 'New node revision has changed data.'); + $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host did.'); + + // Make sure that there are only 2 revisions. + $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); + $this->assertEqual($node_revisions_count, 2); + + // Revert to first revision of the node. + $node = $this->entityTypeManager->getStorage('node')->loadRevision($original_node_revision); + $node->setNewRevision(); + $node->isDefaultRevision(TRUE); + $node->save(); + $node = node_load($node->id(), TRUE); + // Check the revision of the node. + $this->assertNotEqual('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.'); + $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.'); + + // Test that removing/changing composite references results in translation + // changes. + $node->set('composite_reference', []); + $this->assertTrue($node->hasTranslationChanges()); + + // Revert the changes to avoid interfering with the delete test. + $node->set('composite_reference', $composite); + + // Test that the composite entity is deleted when its parent is deleted. + $node->delete(); + $this->assertNull(EntityTestCompositeRelationship::load($composite->id())); + } + + /** + * Tests composite relationship with translations and an untranslatable field. + */ + function testCompositeRelationshipWithTranslationNonTranslatableField() { + + ConfigurableLanguage::createFromLangcode('de')->save(); + + // Create the test composite entity with a translation. + $composite = EntityTestCompositeRelationship::create(array( + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + )); + $composite->addTranslation('de', $composite->toArray()); + $composite->save(); + + + // Create a node with a reference to the test composite entity. + $node = Node::create(array( + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $composite, + )); + $node->addTranslation('de', $node->toArray()); + $node->save(); + + // Verify the value of parent type and id after create a node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); + $this->assertEqual($composite->parent_id->value, $node->id()); + $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + $this->assertTrue($composite->hasTranslation('de')); + + // Test that the composite entity is not deleted when the german translation + // of the parent is deleted. + $node->removeTranslation('de'); + $node->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + $this->assertFalse($composite->hasTranslation('de')); + + // Change the language of the entity, ensure that doesn't try to delete + // the default translation. + $node->set('langcode', 'de'); + $node->save(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Test that the composite entity is deleted when its parent is deleted. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Tests composite relationship with translations and a translatable field. + */ + function testCompositeRelationshipWithTranslationTranslatableField() { + $field_config = FieldConfig::loadByName('node', 'article', 'composite_reference'); + $field_config->setTranslatable(TRUE); + $field_config->save(); + + ConfigurableLanguage::createFromLangcode('de')->save(); + + // Create the test composite entity with a translation. + $composite = EntityTestCompositeRelationship::create(array( + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + )); + $composite->addTranslation('de', $composite->toArray()); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create(array( + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $composite, + )); + $node->addTranslation('de', $node->toArray()); + $node->save(); + + // Verify the value of parent type and id after create a node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); + $this->assertEqual($composite->parent_id->value, $node->id()); + $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + + // Test that the composite entity is not when the german translation of the parent is deleted. + $node->removeTranslation('de'); + $node->save(); + //$this->entityTypeManager->getStorage('entity_test_composite')->resetCache(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Test that the composite entity is deleted when its parent is deleted. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + // @todo Support deletions for translatable fields. + // @see https://www.drupal.org/node/2834374 + // $this->assertNull($composite); + } + + /** + * Tests composite relationship with revisions. + */ + function testCompositeRelationshipWithRevisions() { + + // Create the test composite entity with a translation. + $composite = EntityTestCompositeRelationship::create(array( + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + )); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create(array( + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $composite, + )); + $node->save(); + + + // Verify the value of parent type and id after create a node. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $composite_original_revision_id = $composite->getRevisionId(); + $node_original_revision_id = $node->getRevisionId(); + $this->assertEqual($composite->parent_type->value, $node->getEntityTypeId()); + $this->assertEqual($composite->parent_id->value, $node->id()); + $this->assertEqual($composite->parent_field_name->value, 'composite_reference'); + + $node->setNewRevision(TRUE); + $node->save(); + // Ensure that we saved a new revision ID. + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotEqual($composite->getRevisionId(), $composite_original_revision_id); + + // Test that deleting the first revision does not delete the composite. + $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNotNull($composite); + + // Ensure that the composite revision was deleted as well. + $composite_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original_revision_id); + $this->assertNull($composite_revision); + + // Test that the composite entity is deleted when its parent is deleted. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite->id()); + $this->assertNull($composite); + } + + /** + * Tests that the composite revision is not deleted if it is the default one. + */ + function testCompositeRelationshipDefaultRevision() { + // Create a node with a reference to a test composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + ]); + $composite->save(); + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + $composite = EntityTestCompositeRelationship::load($composite->id()); + $composite_original_revision_id = $composite->getRevisionId(); + $node_original_revision_id = $node->getRevisionId(); + + // Set a new revision, composite entity should have a new revision as well. + $node->setNewRevision(TRUE); + $node->save(); + // Ensure that we saved a new revision ID. + $composite2 = EntityTestCompositeRelationship::load($composite->id()); + $composite2_rev_id = $composite2->getRevisionId(); + $this->assertNotEquals($composite2_rev_id, $composite_original_revision_id); + + // Revert default composite entity revision to the original revision. + $composite_original = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original_revision_id); + $composite_original->isDefaultRevision(TRUE); + $composite_original->save(); + // Check the default composite revision is the original composite revision. + $this->assertEquals($composite_original_revision_id, $composite_original->getrevisionId()); + + // Test deleting the first node revision, referencing to the default + // composite revision, does not delete the default composite revision. + $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); + $composite_default = EntityTestCompositeRelationship::load($composite_original->id()); + $this->assertNotNull($composite_default); + $composite_default_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original->getrevisionId()); + $this->assertNotNull($composite_default_revision); + // Ensure the second revision still exists. + $composite2_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite2_rev_id); + $this->assertNotNull($composite2_revision); + } + + /** + * Tests that the composite revision is not deleted if it is still in use. + */ + function testCompositeRelationshipDuplicatedRevisions() { + // Create a node with a reference to a test composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'uuid' => $this->randomMachineName(), + 'name' => $this->randomMachineName(), + ]); + $composite->save(); + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + $composite = EntityTestCompositeRelationship::load($composite->id()); + $composite_original_revision_id = $composite->getRevisionId(); + $node_original_revision_id = $node->getRevisionId(); + + // Set a new revision, composite entity should have a new revision as well. + $node->setNewRevision(TRUE); + $node->save(); + // Ensure that we saved a new revision ID. + $composite2 = EntityTestCompositeRelationship::load($composite->id()); + $composite2_rev_id = $composite2->getRevisionId(); + $this->assertNotEquals($composite2_rev_id, $composite_original_revision_id); + + // Set the new node revision to reference to the original composite + // revision as well to test this composite revision will not be deleted. + $this->database->update('node__composite_reference') + ->fields(['composite_reference_target_revision_id' => $composite_original_revision_id]) + ->condition('revision_id', $node->getRevisionId()) + ->execute(); + $this->database->update('node_revision__composite_reference') + ->fields(['composite_reference_target_revision_id' => $composite_original_revision_id]) + ->condition('revision_id', $node->getRevisionId()) + ->execute(); + + // Test deleting the first revision does not delete the composite. + $this->entityTypeManager->getStorage('node')->deleteRevision($node_original_revision_id); + $composite2 = EntityTestCompositeRelationship::load($composite2->id()); + $this->assertNotNull($composite2); + + // Ensure the original composite revision is not deleted because it is + // still referenced by the second node revision. + $composite_original_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite_original_revision_id); + $this->assertNotNull($composite_original_revision); + // Ensure the second revision still exists. + $composite2_revision = $this->entityTypeManager->getStorage('entity_test_composite')->loadRevision($composite2_rev_id); + $this->assertNotNull($composite2_revision); + + // Test that the composite entity is deleted when its parent is deleted. + $node->delete(); + $composite = EntityTestCompositeRelationship::load($composite2->id()); + $this->assertNull($composite); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php new file mode 100644 index 000000000..4e261bfcf --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php @@ -0,0 +1,351 @@ +save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => TRUE, + )); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create the test composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert the revision count. + $this->assertRevisionCount(1, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + + // Create a translation as a pending revision for both the composite and the + // node. While technically, the referenced composite could be the same + // entity, for translatable fields, it makes more sense if each translation + // points to a separate entity, each only with a single language. + $composite_de = $node->get('composite_reference')->entity->createDuplicate(); + $composite_de->set('langcode', 'de'); + $composite_de->set('name', 'Pending Revision Composite #1 DE'); + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node->addTranslation('de', ['title' => 'Pending Revision Node #1 DE', 'composite_reference' => $composite_de] + $node->toArray()); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $node_de->save(); + + // Assert the revision count. + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + + // The DE translation will now reference to a pending revision of the + // composite entity but the en translation will reference the existing, + // unchanged revision. + /** @var \Drupal\node\NodeInterface $node_revision */ + $node_revision = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_de = $node_revision->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + // The composite is the default revision because it is a new entity. + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a second translation revision for FR. + $composite_fr = $node->get('composite_reference')->entity->createDuplicate(); + $composite_fr->set('langcode', 'fr'); + $composite_fr->set('name', 'Pending Revision Composite #1 FR'); + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR', 'composite_reference' => $composite_fr] + $node->toArray()); + $node_fr->setNewRevision(TRUE); + $node_fr->isDefaultRevision(FALSE); + $node_fr->save(); + + // Assert the revision count. + $this->assertRevisionCount(3, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each has the original revision as parent without + // any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_revision = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_fr = $node_revision->getTranslation('fr'); + $this->assertTrue((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertTrue($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_fr->get('composite_reference')->target_revision_id); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId())->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Now make a change to the initial source revision, save as a new default + // revision. + $initial_revision_id = $node->getRevisionId(); + $node->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $node->setTitle('Updated Source Node'); + $node->setNewRevision(TRUE); + $node->save(); + + // Assert the revision count. + $this->assertRevisionCount(4, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + + // Now publish the FR pending revision. + $node_storage->createRevision($node_fr->getTranslation('fr'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(5, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated english source and + // the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + // Now publish the DE pending revision as well. + $node_storage->createRevision($node_de->getTranslation('de'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(6, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $node_de = $node->getTranslation('de'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + + // Each translation only has the composite in its translation. + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('en')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_de->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id'); + + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php new file mode 100644 index 000000000..fbe4c141f --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php @@ -0,0 +1,459 @@ +save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => FALSE, + ]); + $field->save(); + + // Create an untranslatable field on the composite entity. + $text_field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_untranslatable', + 'entity_type' => 'entity_test_composite', + 'type' => 'string', + ]); + $text_field_storage->save(); + $text_field = FieldConfig::create([ + 'field_storage' => $text_field_storage, + 'bundle' => 'entity_test_composite', + 'translatable' => FALSE, + ]); + $text_field->save(); + + // Add a nested composite field. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'composite_reference', + 'entity_type' => 'entity_test_composite', + 'type' => 'entity_reference_revisions', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'entity_test_composite', + 'translatable' => FALSE, + ]); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create a nested composite entity. + $nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Source Composite', + ]); + $nested_composite->save(); + + // Create a composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + 'field_untranslatable' => 'Initial untranslatable field', + 'composite_reference' => $nested_composite, + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + $initial_revision_id = $node->getRevisionId(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $this->assertRevisionCount(1, $node); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $composite); + // Assert there is no new composite revision after creating a host entity. + $this->assertRevisionCount(1, $nested_composite); + + // Create a second nested composite entity. + $second_nested_composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Nested Composite #2', + ]); + + // Add a pending revision. + $node = $node_storage->createRevision($node, FALSE); + $node->get('composite_reference')->entity->get('composite_reference')->appendItem($second_nested_composite); + $node->save(); + $pending_en_revision_id = $node->getRevisionId(); + + $this->assertRevisionCount(2, $node); + $this->assertRevisionCount(2, $composite); + $this->assertRevisionCount(2, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + // Create a DE translation, start as a draft to replicate the behavior of + // the UI. + $node_de = $node->addTranslation('de', ['title' => 'New Node #1 DE'] + $node->toArray()); + $node_de = $node_storage->createRevision($node_de, FALSE); + + // Despite starting of the draft revision, creating draft of the translation + // uses the paragraphs of the default revision. + $this->assertCount(1, $node_de->get('composite_reference')->entity->get('composite_reference')); + + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Composite #1 DE'); + $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'New Nested Composite #1 DE'); + $node_de->isDefaultRevision(TRUE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getPropertyPath() . ': ' . $violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + + $this->assertRevisionCount(3, $node); + $this->assertRevisionCount(3, $composite); + $this->assertRevisionCount(3, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + // Update the translation as a pending revision for both the composite and + // the node. + $node_de->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Revision Composite #1 DE'); + $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->set('name', 'Pending Nested Composite #1 DE'); + $node_de->set('title', 'Pending Revision Node #1 DE'); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $violations = $node_de->validate(); + foreach ($violations as $violation) { + $this->fail($violation->getMessage()); + } + $this->assertEquals(0, count($violations)); + $node_de->save(); + + $this->assertRevisionCount(4, $node); + $this->assertRevisionCount(4, $composite); + $this->assertRevisionCount(4, $nested_composite); + $this->assertRevisionCount(1, $second_nested_composite); + + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse((bool) $node_de->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_de->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a FR translation, start as a draft to replicate the behavior of + // the UI. + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR'] + $node->toArray()); + $node_fr = $node_storage->createRevision($node_fr, FALSE); + $node_fr->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Revision Composite #1 FR'); + $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->set('name', 'Pending Nested Composite #1 FR'); + $violations = $node_fr->validate(); + $this->assertEquals(0, count($violations)); + $node_fr->save(); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each composite has the original revision as parent + // without any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_fr = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_fr->isDefaultRevision()); + $this->assertTrue($node_fr->hasTranslation('de')); + $this->assertFalse((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_fr->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->getTranslation('fr')->label()); + $this->assertEquals('Initial Source Node', $node_fr->label()); + $this->assertFalse($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Nested Composite #1 FR', $node_fr->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->getTranslation('fr')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_fr->get('composite_reference')->entity->label()); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse($node_de->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node_de->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial untranslatable field', $node_de->get('composite_reference')->entity->getTranslation('de')->get('field_untranslatable')->value); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create another pending EN revision and make that the default. + $node = $node_storage->loadRevision($pending_en_revision_id); + $new_revision = $node_storage->createRevision($node); + $new_revision->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $new_revision->get('composite_reference')->entity->set('field_untranslatable', 'Updated untranslatable field'); + $new_revision->setTitle('Updated Source Node'); + $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->set('name', 'Draft Nested Source Composite #2'); + $violations = $new_revision->validate(); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Initial Nested Source Composite', $node->get('composite_reference')->entity->get('composite_reference')->entity->label()); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + $this->assertEquals('Initial Nested Source Composite', $node_initial->get('composite_reference')->entity->get('composite_reference')->entity->label()); + $this->assertEquals('Initial untranslatable field', $node_initial->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertCount(1, $node_initial->get('composite_reference')->entity->get('composite_reference')); + + // The current node_fr pending revision still has the initial value before + // "merging" it, but it will get the new value for the untranslatable field + // in the new revision. + $node_fr = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertEquals('Initial untranslatable field', $node_fr->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertCount(1, $node_fr->get('composite_reference')->entity->get('composite_reference')); + + // Now publish the FR pending revision and also add a translation for + // the second composite that it now has. + $new_revision = $node_storage->createRevision($node_fr->getTranslation('fr')); + $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference')); + $new_revision->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->set('name', 'FR Nested Composite #2'); + + $violations = $new_revision->validate(); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + $this->assertRevisionCount(7, $node); + $this->assertRevisionCount(7, $composite); + $this->assertRevisionCount(7, $nested_composite); + $this->assertRevisionCount(3, $second_nested_composite); + + // The new default revision should now have the updated english source, + // original german translation and the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Nested Composite #1 FR', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('New Node #1 DE', $node->getTranslation('de')->label()); + $this->assertEquals('New Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('New Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label()); + + // Now publish the DE pending revision as well. + $new_revision = $node_storage->createRevision($node_de->getTranslation('de')); + $violations = $new_revision->validate(); + $this->assertCount(2, $new_revision->get('composite_reference')->entity->get('composite_reference')); + $this->assertEquals(0, count($violations)); + $new_revision->save(); + + $this->assertRevisionCount(8, $node); + $this->assertRevisionCount(8, $composite); + $this->assertRevisionCount(8, $nested_composite); + $this->assertRevisionCount(4, $second_nested_composite); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node->getTranslation('de')->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Pending Nested Composite #1 DE', $node->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + $this->assertEquals('Updated untranslatable field', $node->get('composite_reference')->entity->get('field_untranslatable')->value); + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->label()); + $this->assertEquals('FR Nested Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('fr')->label()); + + // The second nested composite of DE inherited the default values for its + // translation. + $this->assertEquals('Draft Nested Source Composite #2', $node->get('composite_reference')->entity->get('composite_reference')[1]->entity->getTranslation('de')->label()); + + // Simulate creating a new pending revision like + // \Drupal\content_moderation\EntityTypeInfo::entityPrepareForm(). + $new_revision = $node_storage->createRevision($node); + $revision_key = $new_revision->getEntityType()->getKey('revision'); + $new_revision->set($revision_key, $new_revision->getLoadedRevisionId()); + $new_revision->save(); + $this->assertEquals('Pending Nested Composite #1 DE', $new_revision->get('composite_reference')->entity->get('composite_reference')->entity->getTranslation('de')->label()); + + } + + /** + * Asserts the revision count of an entity. + * + * @param int $expected + * The expected amount of revisions. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + */ + protected function assertRevisionCount($expected, EntityInterface $entity) { + $node_revisions_count = \Drupal::entityQuery($entity->getEntityTypeId()) + ->condition($entity->getEntityType()->getKey('id'), $entity->id()) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $node_revisions_count); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php new file mode 100644 index 000000000..430d06e62 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php @@ -0,0 +1,106 @@ + 'article', 'name' => 'Article']; + $node_type = NodeType::create($values); + $node_type->save(); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('system', ['sequences']); + $this->installSchema('node', ['node_access']); + + // Add the entity_reference_revisions field to article. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + $user = $this->createUser(['administer entity_test composite relationship']); + \Drupal::currentUser()->setAccount($user); + } + + public function testFormatterWithDeletedReference() { + // Create the test composite entity. + $text = 'Dummy text'; + $entity_test = EntityTestCompositeRelationship::create([ + 'uuid' => $text, + 'name' => $text, + ]); + $entity_test->save(); + + $text = 'Clever text'; + // Set the name to a new text. + /** @var \Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship $entity_test */ + $entity_test->name = $text; + $entity_test->setNeedsSave(TRUE); + + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $entity_test, + ]); + $node->save(); + + // entity_reference_revisions_entity_view + $result = $node->composite_reference->view(['type' => 'entity_reference_revisions_entity_view']); + $this->setRawContent($this->render($result)); + $this->assertText('Clever text'); + + // Remove the referenced entity. + $entity_test->delete(); + + $node = Node::load($node->id()); + $result = $node->composite_reference->view(['type' => 'entity_reference_revisions_entity_view']); + $this->render($result); + $this->assertNoText('Clever text'); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php new file mode 100644 index 000000000..2b66a6305 --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php @@ -0,0 +1,321 @@ + 'article', 'name' => 'Article']; + $node_type = NodeType::create($values); + $node_type->save(); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('system', ['sequences']); + $this->installSchema('node', ['node_access']); + } + + /** + * Test for NeedsSaveInterface implementation. + * + * Tests that the referenced entity is saved when needsSave() is TRUE. + */ + public function testNeedsSave() { + + // Add the entity_reference_revisions field to article. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + )); + $field->save(); + + $text = 'Dummy text'; + // Create the test composite entity. + $entity_test = EntityTestCompositeRelationship::create(array( + 'uuid' => $text, + 'name' => $text, + )); + $entity_test->save(); + + $text = 'Clever text'; + // Set the name to a new text. + /** @var \Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship $entity_test */ + $entity_test->name = $text; + $entity_test->setNeedsSave(TRUE); + // Create a node with a reference to the test entity and save. + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $entity_test, + ]); + // Check the name is properly set and that getValue() returns the entity + // when it is marked as needs save." + $values = $node->composite_reference->getValue(); + $this->assertTrue(isset($values[0]['entity'])); + static::assertEquals($values[0]['entity']->name->value, $text); + $node->composite_reference->setValue($values); + static::assertEquals($node->composite_reference->entity->name->value, $text); + $node->save(); + + // Check that the name has been updated when the parent has been saved. + /** @var \Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship $entity_test_after */ + $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id()); + static::assertEquals($entity_test_after->name->value, $text); + + $new_text = 'Dummy text again'; + // Set another name and save the node without marking it as needs saving. + $entity_test_after->name = $new_text; + $entity_test_after->setNeedsSave(FALSE); + + // Load the Node and check the composite reference entity is not returned + // from getValue() if it is not marked as needs saving. + $node = Node::load($node->id()); + $values = $node->composite_reference->getValue(); + $this->assertFalse(isset($values[0]['entity'])); + $node->composite_reference = $entity_test_after; + $node->save(); + + // Check the name is not updated. + \Drupal::entityTypeManager()->getStorage('entity_test_composite')->resetCache(); + $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id()); + static::assertEquals($text, $entity_test_after->name->value); + + // Test if after delete the referenced entity there are no problems setting + // the referencing values to the parent. + $entity_test->delete(); + $node = Node::load($node->id()); + $node->save(); + + // Test if the needs save variable is set as false after saving. + $entity_needs_save = EntityTestCompositeRelationship::create([ + 'uuid' => $text, + 'name' => $text, + ]); + $entity_needs_save->setNeedsSave(TRUE); + $entity_needs_save->save(); + $this->assertFalse($entity_needs_save->needsSave()); + } + + /** + * Test for NeedsSaveInterface implementation. + * + * Tests that the fields in the parent are properly updated. + */ + public function testSaveNewEntity() { + // Add the entity_reference_revisions field to article. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + )); + $field->save(); + + $text = 'Dummy text'; + // Create the test entity. + $entity_test = EntityTestCompositeRelationship::create(array( + 'uuid' => $text, + 'name' => $text, + )); + + // Create a node with a reference to the test entity and save. + $node = Node::create([ + 'title' => $this->randomMachineName(), + 'type' => 'article', + 'composite_reference' => $entity_test, + ]); + $validate = $node->validate(); + $this->assertEmpty($validate); + $node->save(); + + // Test that the fields on node are properly set. + $node_after = Node::load($node->id()); + static::assertEquals($node_after->composite_reference[0]->target_id, $entity_test->id()); + static::assertEquals($node_after->composite_reference[0]->target_revision_id, $entity_test->getRevisionId()); + // Check that the entity is not new after save parent. + $this->assertFalse($entity_test->isNew()); + + // Create a new test entity. + $text = 'Smart text'; + $second_entity_test = EntityTestCompositeRelationship::create(array( + 'uuid' => $text, + 'name' => $text, + )); + $second_entity_test->save(); + + // Set the new test entity to the node field. + $node_after->composite_reference = $second_entity_test; + // Check the fields have been updated. + static::assertEquals($node_after->composite_reference[0]->target_id, $second_entity_test->id()); + static::assertEquals($node_after->composite_reference[0]->target_revision_id, $second_entity_test->getRevisionId()); + } + + /** + * Tests entity_reference_revisions default value and config dependencies. + */ + public function testEntityReferenceRevisionsDefaultValue() { + + // Create a test target node used as entity reference by another test node. + $node_target = Node::create([ + 'title' => 'Target node', + 'type' => 'article', + 'body' => 'Target body text', + 'uuid' => '2d04c2b4-9c3d-4fa6-869e-ecb6fa5c9410', + ]); + $node_target->save(); + + // Create an entity reference field to reference to the test target node. + /** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */ + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'target_node_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => ['target_type' => 'node'], + ]); + $field_storage->save(); + /** @var \Drupal\field\Entity\FieldConfig $field */ + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'required' => FALSE, + 'settings' => ['handler_settings' => ['target_bundles' => ['article' => 'article']]], + ]); + // Add reference values to field config that will be used as default value. + $default_value = [ + [ + 'target_id' => $node_target->id(), + 'target_revision_id' => $node_target->getRevisionId(), + 'target_uuid' => $node_target->uuid(), + ], + ]; + $field->setDefaultValue($default_value)->save(); + + // Resave the target node, so that the default revision is not the one we + // want to use. + $revision_id = $node_target->getRevisionId(); + $node_target_after = Node::load($node_target->id()); + $node_target_after->setNewRevision(); + $node_target_after->save(); + $this->assertTrue($node_target_after->getRevisionId() != $revision_id); + + // Create another node. + $node_host = Node::create([ + 'title' => 'Host node', + 'type' => 'article', + 'body' => 'Host body text', + 'target_node_reference' => $node_target, + ]); + $node_host->save(); + + // Check if the ERR default values are properly created. + $node_host_after = Node::load($node_host->id()); + $this->assertEquals($node_host_after->target_node_reference->target_id, $node_target->id()); + $this->assertEquals($node_host_after->target_node_reference->target_revision_id, $revision_id); + + // Check if the configuration dependencies are properly created. + $dependencies = $field->calculateDependencies()->getDependencies(); + $this->assertEquals($dependencies['content'][0], 'node:article:2d04c2b4-9c3d-4fa6-869e-ecb6fa5c9410'); + $this->assertEquals($dependencies['config'][0], 'field.storage.node.target_node_reference'); + $this->assertEquals($dependencies['config'][1], 'node.type.article'); + $this->assertEquals($dependencies['module'][0], 'entity_reference_revisions'); + } + + /** + * Tests FieldType\EntityReferenceRevisionsItem::deleteRevision + */ + public function testEntityReferenceRevisionsDeleteHandleDeletedChild() { + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'node', + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + $child = Node::create([ + 'type' => 'article', + 'title' => 'Child node', + ]); + $child->save(); + + $node = Node::create([ + 'type' => 'article', + 'title' => 'Parent node', + 'field_reference' => [ + [ + 'target_id' => $child->id(), + 'target_revision_id' => $child->getRevisionId(), + ] + ], + ]); + + // Create two revisions. + $node->save(); + $revisionId = $node->getRevisionId(); + $node->setNewRevision(TRUE); + $node->save(); + + // Force delete the child Paragraph. + // Core APIs allow this although it is an inconsistent storage situation + // for Paragraphs. + $child->delete(); + + // Previously deleting a revision with a lost child failed fatal. + \Drupal::entityTypeManager()->getStorage('node')->deleteRevision($revisionId); + } + +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php new file mode 100644 index 000000000..e68cf058c --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php @@ -0,0 +1,46 @@ +installConfig($this->modules); + } + + /** + * Tests deriver. + * + * @covers ::getDerivativeDefinitions + */ + public function testDestinationDeriver() { + /** @var MigrateDestinationPluginManager $migrationDestinationManager */ + $migrationDestinationManager = \Drupal::service('plugin.manager.migrate.destination'); + + $destination = $migrationDestinationManager->getDefinition('entity_reference_revisions:entity_test_composite'); + $this->assertEquals(EntityReferenceRevisions::class, $destination['class']); + } + + + +} diff --git a/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php new file mode 100644 index 000000000..f6f42fc9d --- /dev/null +++ b/web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php @@ -0,0 +1,540 @@ +installEntitySchema('entity_test_composite'); + $this->installSchema('system', ['sequences']); + $this->installConfig($this->modules); + + $this->migrationPluginManager = \Drupal::service('plugin.manager.migration'); + } + + /** + * Tests get entity type id. + * + * @dataProvider getEntityTypeIdDataProvider + * + * @covers ::getEntityTypeId + */ + public function testGetEntityTypeId(array $definition, $expected) { + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = $this->migrationPluginManager->createStubMigration($definition); + /** @var \Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions $destination */ + $destination = $migration->getDestinationPlugin(); + + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ + $storage = $this->readAttribute($destination, 'storage'); + $actual = $this->readAttribute($storage, 'entityTypeId'); + + $this->assertEquals($expected, $actual); + } + + /** + * Provides multiple migration definitions for "getEntityTypeId" test. + */ + public function getEntityTypeIdDataProvider() { + $data = $this->getEntityDataProvider(); + + foreach ($data as &$datum) { + $datum['expected'] = 'entity_test_composite'; + } + + return $data; + } + + /** + * Tests get entity. + * + * @dataProvider getEntityDataProvider + * + * @covers ::getEntity + * @covers ::rollback + * @covers ::rollbackNonTranslation + */ + public function testGetEntity(array $definition, array $expected) { + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = $this->migrationPluginManager->createStubMigration($definition); + $migrationExecutable = (new MigrateExecutable($migration, $this)); + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ + $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); + // Test inserting and updating by looping twice. + for ($i = 0; $i < 2; $i++) { + $migrationExecutable->import(); + $migration->getIdMap()->prepareUpdate(); + foreach ($expected as $data) { + $entity = $storage->loadRevision($data['revision_id']); + $this->assertEquals($data['label'], $entity->label()); + $this->assertEquals($data['id'], $entity->id()); + } + } + $migrationExecutable->rollback(); + foreach ($expected as $data) { + $entity = $storage->loadRevision($data['id']); + $this->assertEmpty($entity); + } + } + + /** + * Provides multiple migration definitions for "getEntity" test. + */ + public function getEntityDataProvider() { + return [ + 'without keys' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 1b'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 2'], + ], + ], + 'with ids' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ['id' => 3, 'name' => 'content item 3'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + 'id' => 'id', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1b'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'], + ], + ], + 'with ids and new revisions' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + 'id' => 'id', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + 'new_revisions' => TRUE, + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1a'], + ['id' => 1, 'revision_id' => 2, 'label' => 'content item 1b'], + ['id' => 2, 'revision_id' => 3, 'label' => 'content item 2'], + ], + ], + 'with ids and revisions' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'revision_id' => 1, 'name' => 'content item 1'], + ['id' => 2, 'revision_id' => 2, 'name' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'name' => 'content item 3'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'id' => 'id', + 'revision_id' => 'revision_id', + 'name' => 'name', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'revision_id' => 1, 'label' => 'content item 1'], + ['id' => 2, 'revision_id' => 2, 'label' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'label' => 'content item 3'], + ], + ], + ]; + } + + /** + * Tests multi-value and single-value destination field linkage. + * + * @dataProvider destinationFieldMappingDataProvider + */ + public function testDestinationFieldMapping(array $data) { + $this->enableModules(['node', 'field']); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('node', ['node_access']); + + // Create new content type. + $values = ['type' => 'article', 'name' => 'Article']; + $node_type = NodeType::create($values); + $node_type->save(); + + // Add the field_err_single field to the node type. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_err_single', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'entity_test_composite', + ], + 'cardinality' => 1, + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + // Add the field_err_multiple field to the node type. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_err_multiple', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'entity_test_composite', + ], + 'cardinality' => -1, + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + $definitions = []; + $instances = []; + foreach ($data as $datum) { + $definitions[$datum['definition']['id']] = $datum['definition']; + $instances[$datum['definition']['id']] = $this->migrationPluginManager->createStubMigration($datum['definition']); + } + + // Reflection is easier than mocking. We need to use createInstance for + // purposes of registering the migration for the migration process plugin. + $reflector = new \ReflectionObject($this->migrationPluginManager); + $property = $reflector->getProperty('definitions'); + $property->setAccessible(TRUE); + $property->setValue($this->migrationPluginManager, $definitions); + $this->container->set('plugin.manager.migration', $this->migrationPluginManager); + + foreach ($data as $datum) { + $migration = $this->migrationPluginManager->createInstance($datum['definition']['id']); + $migrationExecutable = (new MigrateExecutable($migration, $this)); + /** @var \Drupal\Core\Entity\EntityStorageBase $storage */ + $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); + $migrationExecutable->import(); + foreach ($datum['expected'] as $expected) { + $entity = $storage->loadRevision($expected['id']); + $properties = array_diff_key($expected, array_flip(['id'])); + foreach ($properties as $property => $value) { + if (is_array($value)) { + foreach ($value as $delta => $text) { + $this->assertNotEmpty($entity->{$property}[$delta]->entity, "Entity property $property with $delta is empty"); + $this->assertEquals($text, $entity->{$property}[$delta]->entity->label()); + } + } + else { + $this->assertNotEmpty($entity, 'Entity with label ' . $expected[$property] . ' is empty'); + $this->assertEquals($expected[$property], $entity->label()); + } + } + } + } + } + + /** + * Provides multiple migration definitions for "getEntity" test. + */ + public function destinationFieldMappingDataProvider() { + return [ + 'scenario 1' => [ + [ + 'single err' => [ + 'definition' => [ + 'id' => 'single_err', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'photo' => 'Photo1 here', + ], + [ + 'id' => 2, + 'photo' => 'Photo2 here', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'name' => 'photo', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'name' => 'Photo1 here'], + ['id' => 2, 'name' => 'Photo2 here'], + ], + ], + 'multiple err author1' => [ + 'definition' => [ + 'id' => 'multiple_err_author1', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'author' => 'Author 1', + ], + [ + 'id' => 2, + 'author' => 'Author 2', + ], + ], + 'ids' => [ + 'author' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'author', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 3, 'name' => 'Author 1'], + ['id' => 4, 'name' => 'Author 2'], + ], + ], + 'multiple err author 2' => [ + 'definition' => [ + 'id' => 'multiple_err_author2', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'author' => 'Author 3', + ], + [ + 'id' => 2, + 'author' => 'Author 4', + ], + ], + 'ids' => [ + 'author' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'author', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 5, 'name' => 'Author 3'], + ['id' => 6, 'name' => 'Author 4'], + ], + ], + 'destination entity' => [ + 'definition' => [ + 'id' => 'node_migration', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'Article 1', + 'photo' => 'Photo1 here', + 'author' => ['Author 1', 'Author 3'], + ], + [ + 'id' => 2, + 'title' => 'Article 2', + 'photo' => 'Photo2 here', + 'author' => ['Author 2', 'Author 4'], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => 'article', + ], + 'field_err_single/target_id' => [ + [ + 'plugin' => 'migration', + 'migration' => ['single_err'], + 'no_stub' => TRUE, + 'source' => 'id', + ], + [ + 'plugin' => 'extract', + 'index' => [ + '0', + ], + ], + ], + 'field_err_single/target_revision_id' => [ + [ + 'plugin' => 'migration', + 'migration' => ['single_err'], + 'no_stub' => TRUE, + 'source' => 'id', + ], + [ + 'plugin' => 'extract', + 'index' => [ + 1, + ], + ], + ], + 'field_err_multiple' => [ + [ + 'plugin' => 'migration', + 'migration' => [ + 'multiple_err_author1', + 'multiple_err_author2', + ], + 'no_stub' => TRUE, + 'source' => 'author', + ], + [ + 'plugin' => 'iterator', + 'process' => [ + 'target_id' => '0', + 'target_revision_id' => '1', + ], + ], + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + [ + 'id' => 1, + 'title' => 'Article 1', + 'field_err_single' => ['Photo1 here'], + 'field_err_multiple' => ['Author 1', 'Author 3'], + ], + [ + 'id' => 2, + 'title' => 'Article 2', + 'field_err_single' => ['Photo2 here'], + 'field_err_multiple' => ['Author 2', 'Author 4'], + ], + ], + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function display($message, $type = 'status') { + $this->assertTrue($type == 'status', $message); + } + +} -- 2.30.2