Added Entity and Entity Reference Revisions which got dropped somewhere along the...
authorJeff Veit <jeff.veit@gmail.com>
Tue, 13 Nov 2018 22:21:54 +0000 (22:21 +0000)
committerJeff Veit <jeff.veit@gmail.com>
Tue, 13 Nov 2018 22:21:54 +0000 (22:21 +0000)
145 files changed:
composer.json
composer.lock
vendor/composer/autoload_files.php
vendor/composer/autoload_static.php
vendor/composer/installed.json
web/modules/contrib/entity/.travis.yml [new file with mode: 0644]
web/modules/contrib/entity/LICENSE.txt [new file with mode: 0644]
web/modules/contrib/entity/README.txt [new file with mode: 0644]
web/modules/contrib/entity/composer.json [new file with mode: 0644]
web/modules/contrib/entity/config/schema/entity.schema.yml [new file with mode: 0644]
web/modules/contrib/entity/entity.info.yml [new file with mode: 0644]
web/modules/contrib/entity/entity.links.action.yml [new file with mode: 0644]
web/modules/contrib/entity/entity.links.task.yml [new file with mode: 0644]
web/modules/contrib/entity/entity.module [new file with mode: 0644]
web/modules/contrib/entity/entity.permissions.yml [new file with mode: 0644]
web/modules/contrib/entity/entity.services.yml [new file with mode: 0644]
web/modules/contrib/entity/entity.views.inc [new file with mode: 0644]
web/modules/contrib/entity/src/Access/EntityRevisionRouteAccessChecker.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundleEntityAccessControlHandler.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundleFieldDefinition.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandlerInterface.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstallerInterface.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php [new file with mode: 0644]
web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php [new file with mode: 0644]
web/modules/contrib/entity/src/Controller/RevisionControllerTrait.php [new file with mode: 0644]
web/modules/contrib/entity/src/Controller/RevisionOverviewController.php [new file with mode: 0644]
web/modules/contrib/entity/src/Entity/RevisionableEntityBundleInterface.php [new file with mode: 0644]
web/modules/contrib/entity/src/EntityAccessControlHandler.php [new file with mode: 0644]
web/modules/contrib/entity/src/EntityAccessControlHandlerBase.php [new file with mode: 0644]
web/modules/contrib/entity/src/EntityPermissionProvider.php [new file with mode: 0644]
web/modules/contrib/entity/src/EntityPermissionProviderBase.php [new file with mode: 0644]
web/modules/contrib/entity/src/EntityPermissionProviderInterface.php [new file with mode: 0644]
web/modules/contrib/entity/src/EntityPermissions.php [new file with mode: 0644]
web/modules/contrib/entity/src/EntityViewBuilder.php [new file with mode: 0644]
web/modules/contrib/entity/src/Form/DeleteMultipleForm.php [new file with mode: 0644]
web/modules/contrib/entity/src/Form/RevisionRevertForm.php [new file with mode: 0644]
web/modules/contrib/entity/src/Form/RevisionableContentEntityForm.php [new file with mode: 0644]
web/modules/contrib/entity/src/Menu/EntityAddLocalAction.php [new file with mode: 0644]
web/modules/contrib/entity/src/Menu/EntityCollectionLocalActionProvider.php [new file with mode: 0644]
web/modules/contrib/entity/src/Menu/EntityLocalActionProviderInterface.php [new file with mode: 0644]
web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php [new file with mode: 0644]
web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php [new file with mode: 0644]
web/modules/contrib/entity/src/Plugin/Derivative/EntityActionsDeriver.php [new file with mode: 0644]
web/modules/contrib/entity/src/Plugin/Derivative/RevisionsOverviewDeriver.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/Condition.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/ConditionGroup.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/EntityQueryAlter.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/QueryAccessEvent.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/QueryAccessHandler.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerBase.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/QueryAccessHandlerInterface.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/UncacheableQueryAccessHandler.php [new file with mode: 0644]
web/modules/contrib/entity/src/QueryAccess/ViewsQueryAlter.php [new file with mode: 0644]
web/modules/contrib/entity/src/Revision/RevisionableContentEntityBase.php [new file with mode: 0644]
web/modules/contrib/entity/src/Routing/AdminHtmlRouteProvider.php [new file with mode: 0644]
web/modules/contrib/entity/src/Routing/DefaultHtmlRouteProvider.php [new file with mode: 0644]
web/modules/contrib/entity/src/Routing/DeleteMultipleRouteProvider.php [new file with mode: 0644]
web/modules/contrib/entity/src/Routing/RevisionRouteProvider.php [new file with mode: 0644]
web/modules/contrib/entity/src/UncacheableEntityAccessControlHandler.php [new file with mode: 0644]
web/modules/contrib/entity/src/UncacheableEntityPermissionProvider.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/entity_module_bundle_plugin_examples_test.info.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_examples_test/src/Plugin/BundlePluginTest/Second.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.info.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/entity_module_bundle_plugin_test.services.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Annotation/BundlePluginTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Entity/EntityTestBundlePlugin.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.first.assigned.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.field.entity_test_enhanced.second.assigned.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/config/install/field.storage.entity_test_enhanced.assigned.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_revisions.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/config/optional/views.view.entity_test_enhanced_with_owner_revisions.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.info.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.links.task.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.module [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.services.yml [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntityWithOwner.php [new file with mode: 0644]
web/modules/contrib/entity/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Functional/CollectionRouteAccessTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Functional/DeleteMultipleFormTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Functional/Menu/EntityLocalActionTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Functional/RevisionRouteAccessTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/BundlePluginTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/DeleteActionTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/QueryAccess/ConditionGroupTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessEventTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessHandlerTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/QueryAccess/QueryAccessTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessHandlerTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/QueryAccess/UncacheableQueryAccessTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/RevisionBasicUITest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Kernel/RevisionOverviewIntegrationTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Unit/BundleEntityAccessControlHandlerTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Unit/EntityAccessControlHandlerTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Unit/EntityPermissionProviderTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Unit/QueryAccess/ConditionTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Unit/UncacheableEntityAccessControlHandlerTest.php [new file with mode: 0644]
web/modules/contrib/entity/tests/src/Unit/UncacheableEntityPermissionProviderTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/LICENSE.txt [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.schema.yml [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/config/schema/entity_reference_revisions.views.schema.yml [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/entity_reference_revisions.info.yml [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/entity_reference_revisions.module [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/entity_reference_revisions.views.inc [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveInterface.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsFieldItemList.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/EntityReferenceRevisionsServiceProvider.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Normalizer/EntityReferenceRevisionItemNormalizer.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityReferenceRevisions.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/DataType/EntityRevisionsAdapter.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsEntityFormatter.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldFormatter/EntityReferenceRevisionsFormatterBase.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/Field/FieldWidget/EntityReferenceRevisionsAutocompleteWidget.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/diff/Field/EntityReferenceRevisionsFieldDiffBuilder.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/migrate/destination/EntityReferenceRevisions.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/views/display/EntityReferenceRevisions.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/views/row/EntityReferenceRevisions.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Plugin/views/style/EntityReferenceRevisions.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAdminTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsAutocompleteTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsCoreVersionUiTestTrait.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsDiffTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/Tests/EntityReferenceRevisionsNormalizerTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/src/TypedData/EntityRevisionDataDefinition.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.info.yml [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/entity_composite_relationship_test.permissions.yml [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsFormatterTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php [new file with mode: 0644]
web/modules/contrib/entity_reference_revisions/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php [new file with mode: 0644]

index 5e6145feca5cd55d89e2a2890e0aad2ac01b4fa8..c514b311020fb3ac0801bdb4041a3de9da5ec65a 100644 (file)
@@ -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",
index ccd2b1cc1d8a41dc09112918cdcdd69974720574..38cdf935ff9010faee6dd064b88901330ea40356 100644 (file)
@@ -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",
                 "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",
                 "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",
index 9efa99a866b29806775b3464cbe21afbcad4634f..981c1609113a4b0aa561b094ba16ac3184db3c01 100644 (file)
@@ -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',
 );
index df87bb0e14f235ac29afa76643484f61b70a4bd1..969c0c0c841cf38d630aa81e95a86042561fbd18 100644 (file)
@@ -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 (
index d8035451f75236e4dadc188718c6e8ef1a3b27de..4240b09b1147c94781cce3b17deb9f3baa20ee43 100644 (file)
             "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",
             "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 (file)
index 0000000..e48d5d7
--- /dev/null
@@ -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 (file)
index 0000000..d159169
--- /dev/null
@@ -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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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.
+
+  <signature of Ty Coon>, 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 (file)
index 0000000..c43f96c
--- /dev/null
@@ -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 (file)
index 0000000..ab1420a
--- /dev/null
@@ -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 (file)
index 0000000..b3c1923
--- /dev/null
@@ -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 (file)
index 0000000..473f3ed
--- /dev/null
@@ -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 (file)
index 0000000..5e02edc
--- /dev/null
@@ -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 (file)
index 0000000..8116ae3
--- /dev/null
@@ -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 (file)
index 0000000..546f322
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Provides expanded entity APIs.
+ */
+
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\entity\BundlePlugin\BundlePluginHandler;
+use Drupal\entity\QueryAccess\EntityQueryAlter;
+use Drupal\entity\QueryAccess\ViewsQueryAlter;
+use Drupal\views\Plugin\views\query\QueryPluginBase;
+use Drupal\views\Plugin\views\query\Sql;
+use Drupal\views\ViewExecutable;
+
+/**
+ * Gets the entity types which use bundle plugins.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeInterface[]
+ *   The entity types.
+ */
+function entity_get_bundle_plugin_entity_types() {
+  $entity_types = \Drupal::entityTypeManager()->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 (file)
index 0000000..1676e88
--- /dev/null
@@ -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 (file)
index 0000000..3e690d1
--- /dev/null
@@ -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 (file)
index 0000000..3074175
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Implements hook_views_data().
+ */
+function entity_views_data() {
+  $entity_types = \Drupal::entityTypeManager()->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 (file)
index 0000000..799af14
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+
+namespace Drupal\entity\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Checks access to a entity revision.
+ */
+class EntityRevisionRouteAccessChecker implements AccessInterface {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Stores calculated access check results.
+   *
+   * @var array
+   */
+  protected $accessCache = [];
+
+  /**
+   * The currently active route match object.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Creates a new EntityRevisionRouteAccessChecker instance.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The currently active route match object.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match) {
+    $this->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 (file)
index 0000000..be9bba5
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Controls access to bundle entities.
+ *
+ * Allows the bundle entity label to be viewed if the account has
+ * access to view entities of that bundle.
+ */
+class BundleEntityAccessControlHandler extends CoreEntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $viewLabelOperation = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    if ($operation === 'view label') {
+      $bundle = $entity->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 (file)
index 0000000..9da74c8
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * Provides a field definition class for bundle fields.
+ *
+ * Core currently doesn't provide one, the hook_entity_bundle_field_info()
+ * example uses BaseFieldDefinition, which is wrong. Tracked in #2346347.
+ *
+ * Note that this class implements both FieldStorageDefinitionInterface and
+ * FieldDefinitionInterface. This is a simplification for DX reasons,
+ * allowing code to return just the bundle definitions instead of having to
+ * return both storage definitions and bundle definitions.
+ */
+class BundleFieldDefinition extends BaseFieldDefinition {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isBaseField() {
+    return FALSE;
+  }
+
+}
diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginHandler.php
new file mode 100644 (file)
index 0000000..e922332
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\entity\BundlePlugin;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class BundlePluginHandler implements BundlePluginHandlerInterface {
+
+  /**
+   * The entity type.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * The bundle plugin manager.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs a new BundlePluginHandler object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
+   *   The bundle plugin manager.
+   */
+  public function __construct(EntityTypeInterface $entity_type, PluginManagerInterface $plugin_manager) {
+    $this->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 (file)
index 0000000..5702fb5
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\entity\BundlePlugin;
+
+use Drupal\Core\Entity\EntityHandlerInterface;
+
+/**
+ * Handles plugin-provided bundles.
+ */
+interface BundlePluginHandlerInterface extends EntityHandlerInterface {
+
+  /**
+   * Gets the bundle info.
+   *
+   * @return array
+   *   An array of bundle information keyed by the bundle name.
+   *   The format expected by hook_entity_bundle_info().
+   */
+  public function getBundleInfo();
+
+  /**
+   * Gets the field storage definitions.
+   */
+  public function getFieldStorageDefinitions();
+
+  /**
+   * Gets the field definitions for a specific bundle.
+   *
+   * @param string $bundle
+   *   The bundle name.
+   *
+   * @return \Drupal\entity\BundleFieldDefinition[]
+   *   An array of bundle field definitions, keyed by field name.
+   */
+  public function getFieldDefinitions($bundle);
+
+}
diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInstaller.php
new file mode 100644 (file)
index 0000000..555407f
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\entity\BundlePlugin;
+
+use Drupal\Core\Entity\EntityBundleListenerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionListenerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
+
+class BundlePluginInstaller implements BundlePluginInstallerInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The entity bundle listener.
+   *
+   * @var \Drupal\Core\Entity\EntityBundleListenerInterface
+   */
+  protected $entityBundleListener;
+
+  /**
+   * The field storage definition listener.
+   *
+   * @var \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
+   */
+  protected $fieldStorageDefinitionListener;
+
+  /**
+   * The field definition listener.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionListenerInterface
+   */
+  protected $fieldDefinitionListener;
+
+  /**
+   * Constructs a new BundlePluginInstaller object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityBundleListenerInterface $entity_bundle_listener
+   *   The entity bundle listener.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionListenerInterface $field_storage_definition_listener
+   *   The field storage definition listener.
+   * @param \Drupal\Core\Field\FieldDefinitionListenerInterface $field_definition_listener
+   *   The field definition listener.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityBundleListenerInterface $entity_bundle_listener, FieldStorageDefinitionListenerInterface $field_storage_definition_listener, FieldDefinitionListenerInterface $field_definition_listener) {
+    $this->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 (file)
index 0000000..afeacee
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\entity\BundlePlugin;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Installs and uninstalls bundle plugins.
+ *
+ * Ensures that the fields provided by the bundle plugins are created/deleted.
+ */
+interface BundlePluginInstallerInterface {
+
+  /**
+   * Installs the bundle plugins provided by the specified modules.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   * @param array $modules
+   *   The modules.
+   */
+  public function installBundles(EntityTypeInterface $entity_type, array $modules);
+
+  /**
+   * Uninstalls the bundle plugins provided by the specified modules.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   * @param array $modules
+   *   The modules.
+   */
+  public function uninstallBundles(EntityTypeInterface $entity_type, array $modules);
+
+}
diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginInterface.php
new file mode 100644 (file)
index 0000000..b79e65f
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\entity\BundlePlugin;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Interface for plugins which act as entity bundles.
+ */
+interface BundlePluginInterface extends PluginInspectionInterface {
+
+  /**
+   * Builds the field definitions for entities of this bundle.
+   *
+   * Important:
+   * Field names must be unique across all bundles.
+   * It is recommended to prefix them with the bundle name (plugin ID).
+   *
+   * @return \Drupal\entity\BundleFieldDefinition[]
+   *   An array of bundle field definitions, keyed by field name.
+   */
+  public function buildFieldDefinitions();
+
+}
diff --git a/web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php b/web/modules/contrib/entity/src/BundlePlugin/BundlePluginUninstallValidator.php
new file mode 100644 (file)
index 0000000..c21f625
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\entity\BundlePlugin;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Prevents uninstalling modules with bundle plugins in case of found data.
+ */
+class BundlePluginUninstallValidator implements ModuleUninstallValidatorInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs the object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
+    $this->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 (file)
index 0000000..6be025a
--- /dev/null
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\entity\Controller;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Defines a trait for common revision UI functionality.
+ */
+trait RevisionControllerTrait {
+
+  /**
+   * Returns the entity type manager.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  abstract protected function entityTypeManager();
+
+  /**
+   * Returns the langauge manager.
+   *
+   * @return \Drupal\Core\Language\LanguageManagerInterface
+   */
+  abstract public function languageManager();
+
+  /**
+   * Determines if the user has permission to revert revisions.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to check revert access for.
+   *
+   * @return bool
+   *   TRUE if the user has revert access.
+   */
+  abstract protected function hasRevertRevisionAccess(EntityInterface $entity);
+
+  /**
+   * Determines if the user has permission to delete revisions.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to check delete revision access for.
+   *
+   * @return bool
+   *   TRUE if the user has delete revision access.
+   */
+  abstract protected function hasDeleteRevisionAccess(EntityInterface $entity);
+
+  /**
+   * Builds a link to revert an entity revision.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity_revision
+   *   The entity to build a revert revision link for.
+   *
+   * @return array
+   *   A link render array.
+   */
+  abstract protected function buildRevertRevisionLink(EntityInterface $entity_revision);
+
+  /**
+   * Builds a link to delete an entity revision.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity_revision
+   *   The entity to build a delete revision link for.
+   *
+   * @return array
+   *   A link render array.
+   */
+  abstract protected function buildDeleteRevisionLink(EntityInterface $entity_revision);
+
+  /**
+   * Returns a string providing details of the revision.
+   *
+   * E.g. Node describes its revisions using {date} by {username}. For the
+   *   non-current revision, it also provides a link to view that revision.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $revision
+   *   The entity revision.
+   * @param bool $is_current
+   *   TRUE if the revision is the current revision.
+   *
+   * @return string
+   *   Returns a string to provide the details of the revision.
+   */
+  abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE);
+
+  /**
+   * Loads all revision IDs of an entity sorted by revision ID descending.
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity.
+   *
+   * @return mixed[]
+   */
+  protected function revisionIds(ContentEntityInterface $entity) {
+    $entity_type = $entity->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' => '<em>',
+              '#markup' => $this->t('Current revision'),
+              '#suffix' => '</em>',
+            ],
+          ];
+          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 (file)
index 0000000..495b6a0
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+
+namespace Drupal\entity\Controller;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Entity\RevisionLogInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a controller which shows the revision history.
+ *
+ * This controller leverages the revision controller trait, which is agnostic to
+ * any entity type, by using \Drupal\Core\Entity\RevisionLogInterface.
+ */
+class RevisionOverviewController extends ControllerBase {
+
+  use RevisionControllerTrait;
+
+  /**
+   * The date formatter.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Creates a new RevisionOverviewController instance.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter.
+   */
+  public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer) {
+    $this->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 %}<p class="revision-log">{{ message }}</p>{% endif %}';
+    }
+    else {
+      $template = '{% trans %} {{ date }} {% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% 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 (file)
index 0000000..18d392a
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\entity\Entity;
+
+use Drupal\Core\Entity\RevisionableEntityBundleInterface as CoreRevisionableEntityBundleInterface;
+
+@trigger_error('\Drupal\entity\Entity\RevisionableEntityBundleInterface has been deprecated in favor of \Drupal\Core\Entity\RevisionableEntityBundleInterface. Use that instead.');
+
+/**
+ * @deprecated in favor of
+ *   \Drupal\Core\Entity\RevisionableEntityBundleInterface. Use that instead.
+ */
+interface RevisionableEntityBundleInterface extends CoreRevisionableEntityBundleInterface {
+}
diff --git a/web/modules/contrib/entity/src/EntityAccessControlHandler.php b/web/modules/contrib/entity/src/EntityAccessControlHandler.php
new file mode 100644 (file)
index 0000000..01f8fd6
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Controls access based on the generic entity permissions.
+ *
+ * @see \Drupal\entity\UncacheableEntityPermissionProvider
+ */
+class EntityAccessControlHandler extends EntityAccessControlHandlerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(EntityTypeInterface $entity_type) {
+    parent::__construct($entity_type);
+
+    if (!$entity_type->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 (file)
index 0000000..c0a63c7
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * @internal
+ */
+class EntityAccessControlHandlerBase extends CoreEntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    $account = $this->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 (file)
index 0000000..70e791e
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Provides generic entity permissions.
+ *
+ * Intended for content entity types, since config entity types usually rely
+ * on a single "administer" permission.
+ *
+ * Provided permissions:
+ * - administer $entity_type
+ * - access $entity_type overview
+ * - view own unpublished $entity_type
+ * - view ($bundle) $entity_type
+ * - update (own|any) ($bundle) $entity_type
+ * - delete (own|any) ($bundle) $entity_type
+ * - create $bundle $entity_type
+ *
+ * Does not provide "view own ($bundle) $entity_type" permissions, because
+ * they require caching pages per user. Please use
+ * \Drupal\entity\UncacheableEntityPermissionProvider if those permissions
+ * are necessary.
+ *
+ * Example annotation:
+ * @code
+ *  handlers = {
+ *    "access" = "Drupal\entity\EntityAccessControlHandler",
+ *    "permission_provider" = "Drupal\entity\EntityPermissionProvider",
+ *  }
+ * @endcode
+ *
+ * @see \Drupal\entity\EntityAccessControlHandler
+ * @see \Drupal\entity\EntityPermissions
+ */
+class EntityPermissionProvider extends EntityPermissionProviderBase {
+
+  /**
+   * 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) {
+    $permissions = parent::buildEntityTypePermissions($entity_type);
+    $entity_type_id = $entity_type->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 (file)
index 0000000..c48e456
--- /dev/null
@@ -0,0 +1,233 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Entity\EntityHandlerInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\user\EntityOwnerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @internal
+ */
+class EntityPermissionProviderBase implements EntityPermissionProviderInterface, EntityHandlerInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type bundle info.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $entityTypeBundleInfo;
+
+  /**
+   * Constructs a new EntityPermissionProvider object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+   *   The entity type bundle info.
+   */
+  public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
+    $this->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 (file)
index 0000000..50f3cca
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Allows entity types to provide permissions.
+ */
+interface EntityPermissionProviderInterface {
+
+  /**
+   * Builds permissions for the given entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return array
+   *   The permissions.
+   */
+  public function buildPermissions(EntityTypeInterface $entity_type);
+
+}
diff --git a/web/modules/contrib/entity/src/EntityPermissions.php b/web/modules/contrib/entity/src/EntityPermissions.php
new file mode 100644 (file)
index 0000000..c2c13a5
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Generates entity permissions via their permission providers.
+ *
+ * @see \Drupal\entity\EntityPermissionProvider
+ * @see \Drupal\entity\UncacheableEntityPermissionProvider
+ */
+class EntityPermissions implements ContainerInjectionInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new EntityPermissions object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->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 (file)
index 0000000..1efe0eb
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Entity\EntityViewBuilder as CoreEntityViewBuilder;
+
+@trigger_error('\Drupal\entity\EntityViewBuilder has been deprecated in favor of \Drupal\Core\Entity\EntityViewBuilder. Use that instead.');
+
+/**
+ * Provides a entity view builder with contextual links support.
+ *
+ * @deprecated in favor of \Drupal\Core\Entity\EntityViewBuilder. Use that
+ *   instead.
+ */
+class EntityViewBuilder extends CoreEntityViewBuilder {
+
+}
diff --git a/web/modules/contrib/entity/src/Form/DeleteMultipleForm.php b/web/modules/contrib/entity/src/Form/DeleteMultipleForm.php
new file mode 100644 (file)
index 0000000..2a7dbc2
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\entity\Form;
+
+use Drupal\Core\Entity\Form\DeleteMultipleForm as CoreDeleteMultipleForm;
+
+@trigger_error('\Drupal\entity\Form\DeleteMultipleForm has been deprecated in favor of \Drupal\Core\Entity\Form\DeleteMultipleForm. Use that instead.');
+
+/**
+ * Provides an entities deletion confirmation form.
+ *
+ * @deprecated Use \Drupal\Core\Entity\Form\DeleteMultipleForm instead.
+ */
+class DeleteMultipleForm extends CoreDeleteMultipleForm {}
diff --git a/web/modules/contrib/entity/src/Form/RevisionRevertForm.php b/web/modules/contrib/entity/src/Form/RevisionRevertForm.php
new file mode 100644 (file)
index 0000000..f4a4a84
--- /dev/null
@@ -0,0 +1,174 @@
+<?php
+
+namespace Drupal\entity\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\RevisionLogInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+class RevisionRevertForm extends ConfirmFormBase {
+
+  /**
+   * The entity revision.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\RevisionLogInterface
+   */
+  protected $revision;
+
+  /**
+   * The date formatter.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The entity bundle information.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleInformation;
+
+  /**
+   * Creates a new RevisionRevertForm instance.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_information
+   *   The bundle information.
+   */
+  public function __construct(DateFormatterInterface $date_formatter, EntityTypeBundleInfoInterface $bundle_information) {
+    $this->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 (file)
index 0000000..6e7d8c7
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+
+namespace Drupal\entity\Form;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Entity\RevisionableEntityBundleInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+@trigger_error('\Drupal\entity\Form\RevisionableContentEntityForm has been deprecated in favor of \Drupal\Core\Entity\ContentEntityForm. Use that instead.');
+
+/**
+ * Extends the base entity form with revision support in the UI.
+ *
+ * @deprecated Use \Drupal\Core\Entity\ContentEntityForm instead.
+ */
+class RevisionableContentEntityForm extends ContentEntityForm {
+
+  /**
+   * The entity being used by this form.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionLogInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareEntity() {
+    parent::prepareEntity();
+
+    $bundle_entity = $this->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 (file)
index 0000000..8b7a04e
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\entity\Menu;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Menu\LocalActionDefault;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides a local action to add an entity.
+ */
+class EntityAddLocalAction extends LocalActionDefault {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * Constructs a EntityAddLocalAction object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+   *   The route provider to load routes by name.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, EntityTypeInterface $entity_type, TranslationInterface $string_translation) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider);
+
+    $this->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 (file)
index 0000000..4ddd554
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\entity\Menu;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Provides a action link to the add page or add form on the collection.
+ */
+class EntityCollectionLocalActionProvider implements EntityLocalActionProviderInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildLocalActions(EntityTypeInterface $entity_type) {
+    $actions = [];
+    if ($entity_type->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 (file)
index 0000000..bba16c3
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\entity\Menu;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Provides an interface for entity local action providers.
+ */
+interface EntityLocalActionProviderInterface {
+
+  /**
+   * Builds local actions for the given entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return array[]
+   *   An array of local action definitions.
+   */
+  public function buildLocalActions(EntityTypeInterface $entity_type);
+
+}
diff --git a/web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php b/web/modules/contrib/entity/src/Plugin/Action/DeleteAction.php
new file mode 100644 (file)
index 0000000..68a8bc5
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\entity\Plugin\Action;
+
+use Drupal\Core\Action\Plugin\Action\DeleteAction as CoreDeleteAction;
+
+@trigger_error('\Drupal\entity\Plugin\Action\DeleteAction has been deprecated in favor of \Drupal\Core\Action\Plugin\Action\DeleteAction. Use that instead.');
+
+/**
+ * Redirects to an entity deletion form.
+ *
+ * @deprecated Use "entity:delete_action" instead.
+ *
+ * @Action(
+ *   id = "entity_delete_action",
+ *   label = @Translation("Delete entity"),
+ *   deriver = "Drupal\entity\Plugin\Action\Derivative\DeleteActionDeriver",
+ * )
+ */
+class DeleteAction extends CoreDeleteAction {}
diff --git a/web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php b/web/modules/contrib/entity/src/Plugin/Action/Derivative/DeleteActionDeriver.php
new file mode 100644 (file)
index 0000000..05bd282
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\entity\Plugin\Action\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a delete action for each content entity type.
+ *
+ * @deprecated
+ */
+class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new DeleteActionDeriver object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->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 (file)
index 0000000..568ad7e
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\entity\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Derives local actions for entity types.
+ */
+class EntityActionsDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs an entity local actions deriver.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->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 (file)
index 0000000..b00b8ce
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\entity\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides local tasks for the revision overview.
+ */
+class RevisionsOverviewDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Creates a new RevisionsOverviewDeriver instance.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entityTypeManager) {
+    $this->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 (file)
index 0000000..ef1f630
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+/**
+ * Represents a single query access condition.
+ */
+final class Condition {
+
+  /**
+   * The supported operators.
+   *
+   * @var string[]
+   */
+  protected static $supportedOperators = [
+    '=', '<>', '<', '<=', '>', '>=', '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 (file)
index 0000000..094feb1
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+
+/**
+ * Represents a group of query access conditions.
+ *
+ * Used by query access handlers for filtering lists of entities based on
+ * granted permissions.
+ *
+ * Examples:
+ * @code
+ *   // Filter by node type and uid.
+ *   $condition_group = new ConditionGroup();
+ *   $condition_group->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 (file)
index 0000000..25c7395
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Query\Sql\Tables;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Defines a class for altering entity queries.
+ *
+ * EntityQuery doesn't have an alter hook, forcing this class to operate
+ * on the underlying SQL query, duplicating the EntityQuery condition logic.
+ *
+ * @internal
+ */
+class EntityQueryAlter implements ContainerInjectionInterface {
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Constructs a new EntityQueryAlter object.
+   *
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, RequestStack $request_stack) {
+    $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('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 (file)
index 0000000..979010b
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Defines the query access event.
+ *
+ * Allows modules to modify access conditions before they're applied to a query.
+ *
+ * The event ID is dynamic: entity.query_access.$entity_type_id
+ */
+class QueryAccessEvent extends Event {
+
+  /**
+   * The conditions.
+   *
+   * @var \Drupal\entity\QueryAccess\ConditionGroup
+   */
+  protected $conditions;
+
+  /**
+   * The operation.
+   *
+   * @var string
+   */
+  protected $operation;
+
+  /**
+   * The user for which to restrict access.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * Constructs a new QueryAccessEvent.
+   *
+   * @param \Drupal\entity\QueryAccess\ConditionGroup $conditions
+   *   The conditions.
+   * @param string $operation
+   *   The operation. Usually one of "view", "update" or "delete".
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to restrict access.
+   */
+  public function __construct(ConditionGroup $conditions, $operation, AccountInterface $account) {
+    $this->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 (file)
index 0000000..f9ec51c
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Controls query access based on the generic entity permissions.
+ *
+ * @see \Drupal\entity\EntityAccessControlHandler
+ * @see \Drupal\entity\EntityPermissionProvider
+ */
+class QueryAccessHandler extends QueryAccessHandlerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildEntityOwnerConditions($operation, AccountInterface $account) {
+    if ($operation == 'view') {
+      // EntityPermissionProvider doesn't provide own/any view permissions.
+      return $this->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 (file)
index 0000000..aaa1315
--- /dev/null
@@ -0,0 +1,268 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Entity\EntityHandlerInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\user\EntityOwnerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Provides common logic for query access handlers.
+ *
+ * @see \Drupal\entity\QueryAccess\QueryAccessHandler
+ * @see \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
+ */
+abstract class QueryAccessHandlerBase implements EntityHandlerInterface, QueryAccessHandlerInterface {
+
+  /**
+   * The entity type.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * The entity type bundle info.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleInfo;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Constructs a new QueryAccessHandlerBase object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
+   *   The entity type bundle info.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EntityTypeBundleInfoInterface $bundle_info, EventDispatcherInterface $event_dispatcher, AccountInterface $current_user) {
+    $this->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 (file)
index 0000000..15d455a
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Query access handlers control access to entities in queries.
+ *
+ * An entity defines a query access handler in its annotation:
+ * @code
+ *   query_access = "\Drupal\entity\QueryAccess\QueryAccessHandler"
+ * @code
+ * The handler builds a set of conditions which are then applied to a query
+ * to filter it. For example, if the user #22 only has access to view
+ * their own entities, a uid = '22' condition will be built and applied.
+ *
+ * The following query types are supported:
+ * - Entity queries with the $entity_type_id . '_access' tag.
+ * - Views queries.
+ */
+interface QueryAccessHandlerInterface {
+
+  /**
+   * Gets the conditions for the given operation and user.
+   *
+   * The "entity.query_access.$entity_type_id" event is fired to allow
+   * modules to alter the conditions.
+   *
+   * @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, or NULL
+   *   to assume the current user. Defaults to NULL.
+   *
+   * @return \Drupal\entity\QueryAccess\ConditionGroup
+   *   The conditions.
+   */
+  public function getConditions($operation, AccountInterface $account = NULL);
+
+}
diff --git a/web/modules/contrib/entity/src/QueryAccess/UncacheableQueryAccessHandler.php b/web/modules/contrib/entity/src/QueryAccess/UncacheableQueryAccessHandler.php
new file mode 100644 (file)
index 0000000..d849fe1
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+/**
+ * Controls query access based on the uncacheable entity permissions.
+ *
+ * @see \Drupal\entity\UncacheableEntityAccessControlHandler
+ * @see \Drupal\entity\UncacheableEntityPermissionProvider
+ */
+class UncacheableQueryAccessHandler extends QueryAccessHandlerBase {}
diff --git a/web/modules/contrib/entity/src/QueryAccess/ViewsQueryAlter.php b/web/modules/contrib/entity/src/QueryAccess/ViewsQueryAlter.php
new file mode 100644 (file)
index 0000000..3d660fd
--- /dev/null
@@ -0,0 +1,251 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Query\Condition as SqlCondition;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Sql\DefaultTableMapping;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\views\Plugin\views\query\Sql;
+use Drupal\views\ViewExecutable;
+use Drupal\views\Views;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Defines a class for altering views queries.
+ *
+ * @internal
+ */
+class ViewsQueryAlter implements ContainerInjectionInterface {
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Constructs a new ViewsQueryAlter object.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   */
+  public function __construct(Connection $connection, EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, RequestStack $request_stack) {
+    $this->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 (file)
index 0000000..f6dc11c
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\entity\Revision;
+
+use Drupal\Core\Entity\RevisionableContentEntityBase as BaseRevisionableContentEntityBase;
+
+/**
+ * Improves the url route handling of core's revisionable content entity base.
+ */
+abstract class RevisionableContentEntityBase extends BaseRevisionableContentEntityBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function urlRouteParameters($rel) {
+    $uri_route_parameters = parent::urlRouteParameters($rel);
+
+    if (strpos($this->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 (file)
index 0000000..7ef2f6e
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\entity\Routing;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider as CoreAdminHtmlRouteProvider;
+
+/**
+ * Provides HTML routes for entities with administrative add/edit/delete pages.
+ */
+class AdminHtmlRouteProvider extends CoreAdminHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCollectionRoute(EntityTypeInterface $entity_type) {
+    $route = parent::getCollectionRoute($entity_type);
+    if ($route && $entity_type->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 (file)
index 0000000..06a3504
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\entity\Routing;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider as CoreDefaultHtmlRouteProvider;
+
+/**
+ * Provides HTML routes for entities.
+ */
+class DefaultHtmlRouteProvider extends CoreDefaultHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCollectionRoute(EntityTypeInterface $entity_type) {
+    $route = parent::getCollectionRoute($entity_type);
+    if ($route && $entity_type->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 (file)
index 0000000..7fb9548
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\entity\Routing;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Provides the HTML route for deleting multiple entities.
+ *
+ * @deprecated Since Drupal 8.6.x the core DefaultHtmlRouteProvider provides
+ *   the route for any entity type with a "delete-multiple-form" link template
+ *   and a "delete-multiple-confirm" form.
+ */
+class DeleteMultipleRouteProvider implements EntityRouteProviderInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $routes = new RouteCollection();
+    if ($route = $this->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 (file)
index 0000000..e341295
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\entity\Routing;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Provides revision routes.
+ */
+class RevisionRouteProvider implements EntityRouteProviderInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $collection = new RouteCollection();
+    $entity_type_id = $entity_type->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 (file)
index 0000000..a53724b
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Controls access based on the uncacheable entity permissions.
+ *
+ * @see \Drupal\entity\UncacheableEntityPermissionProvider
+ *
+ * Note: this access control handler will cause pages to be cached per user.
+ */
+class UncacheableEntityAccessControlHandler extends EntityAccessControlHandlerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(EntityTypeInterface $entity_type) {
+    parent::__construct($entity_type);
+
+    if (!$entity_type->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 (file)
index 0000000..1b25b2a
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * Provides generic entity permissions which are cached per user.
+ *
+ * Intended for content entity types, since config entity types usually rely
+ * on a single "administer" permission.
+ *
+ * Provided permissions:
+ * - administer $entity_type
+ * - access $entity_type overview
+ * - view own unpublished $entity_type
+ * - view (own|any) ($bundle) $entity_type
+ * - update (own|any) ($bundle) $entity_type
+ * - delete (own|any) ($bundle) $entity_type
+ * - create $bundle $entity_type
+ *
+ * Important:
+ * Provides "view own ($bundle) $entity_type" permissions, which require
+ * caching pages per user. This can significantly increase the size of caches,
+ * impacting site performance. Use \Drupal\entity\EntityPermissionProvider
+ * if those permissions are not necessary.
+ *
+ * Example annotation:
+ * @code
+ *  handlers = {
+ *    "access" = "Drupal\entity\UncacheableEntityAccessControlHandler",
+ *    "permission_provider" = "Drupal\entity\UncacheableEntityPermissionProvider",
+ *  }
+ * @endcode
+ *
+ * @see \Drupal\entity\EntityAccessControlHandler
+ * @see \Drupal\entity\EntityPermissions
+ */
+class UncacheableEntityPermissionProvider extends EntityPermissionProviderBase {
+
+  /**
+   * 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) {
+    $permissions = parent::buildEntityTypePermissions($entity_type);
+
+    $entity_type_id = $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,
+        ]),
+      ];
+    }
+
+    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 (file)
index 0000000..0bc69c5
--- /dev/null
@@ -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 (file)
index 0000000..a047b6e
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\entity_module_bundle_plugin_examples_test\Plugin\BundlePluginTest;
+
+use Drupal\entity\BundleFieldDefinition;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest\BundlePluginTestInterface;
+
+/**
+ * Provides the second bundle plugin.
+ *
+ * @BundlePluginTest(
+ *   id = "second",
+ *   label = @Translation("Second"),
+ * )
+ */
+class Second extends PluginBase implements BundlePluginTestInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildFieldDefinitions() {
+    $fields = [];
+    $fields['second_mail'] = BundleFieldDefinition::create('email')
+      ->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 (file)
index 0000000..654a842
--- /dev/null
@@ -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 (file)
index 0000000..ead5cd3
--- /dev/null
@@ -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 (file)
index 0000000..f09f70e
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\entity_module_bundle_plugin_test\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines the BundlePluginTest annotation object.
+ *
+ * Plugin namespace: Plugin\BundlePluginTest.
+ *
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class BundlePluginTest extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The plugin label.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+}
diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/BundlePluginTestManager.php
new file mode 100644 (file)
index 0000000..68cd398
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\entity_module_bundle_plugin_test;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Manages discovery and instantiation of BundlePluginTest plugins.
+ *
+ * @see \Drupal\entity_module_bundle_plugin_test\Annotation\BundlePluginTest
+ * @see plugin_api
+ */
+class BundlePluginTestManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a new BundlePluginTestManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/BundlePluginTest', $namespaces, $module_handler, 'Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest\BundlePluginTestInterface', 'Drupal\entity_module_bundle_plugin_test\Annotation\BundlePluginTest');
+
+    $this->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 (file)
index 0000000..1c75a47
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\entity_module_bundle_plugin_test\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+
+/**
+ * Defines the 'entity_test_bundle_plugin' entity class.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_bundle_plugin",
+ *   label = @Translation("Entity test bundle plugin"),
+ *   bundle_label = @Translation("Bundle Plugin Test"),
+ *   bundle_plugin_type = "bundle_plugin_test",
+ *   base_table = "entity_test_bundle_plugin",
+ *   admin_permission = "administer content",
+ *   fieldable = TRUE,
+ *   entity_keys = {
+ *     "id" = "method_id",
+ *     "uuid" = "uuid",
+ *     "bundle" = "type"
+ *   },
+ * )
+ */
+class EntityTestBundlePlugin extends ContentEntityBase {
+
+}
diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/BundlePluginTestInterface.php
new file mode 100644 (file)
index 0000000..2b25a02
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest;
+
+use Drupal\entity\BundlePlugin\BundlePluginInterface;
+
+/**
+ * Defines the interface for BundlePluginTest plugins.
+ */
+interface BundlePluginTestInterface extends BundlePluginInterface {
+
+}
diff --git a/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php b/web/modules/contrib/entity/tests/modules/entity_module_bundle_plugin_test/src/Plugin/BundlePluginTest/First.php
new file mode 100644 (file)
index 0000000..728a90b
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest;
+
+use Drupal\entity\BundleFieldDefinition;
+use Drupal\Core\Plugin\PluginBase;
+
+/**
+ * Provides the first bundle plugin.
+ *
+ * @BundlePluginTest(
+ *   id = "first",
+ *   label = @Translation("First"),
+ *   description = @Translation("Some description"),
+ * )
+ */
+class First extends PluginBase implements BundlePluginTestInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildFieldDefinitions() {
+    $fields = [];
+    $fields['first_mail'] = BundleFieldDefinition::create('email')
+      ->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 (file)
index 0000000..9e9c577
--- /dev/null
@@ -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 (file)
index 0000000..fb259e3
--- /dev/null
@@ -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 (file)
index 0000000..3669b51
--- /dev/null
@@ -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 (file)
index 0000000..ab0ecf5
--- /dev/null
@@ -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 (file)
index 0000000..98def8a
--- /dev/null
@@ -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 (file)
index 0000000..fcee646
--- /dev/null
@@ -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 (file)
index 0000000..6e3c7e9
--- /dev/null
@@ -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 (file)
index 0000000..3446a3e
--- /dev/null
@@ -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 (file)
index 0000000..e306c10
--- /dev/null
@@ -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 (file)
index 0000000..bcd1492
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Implements hook_entity_bundle_info().
+ */
+function entity_module_test_entity_bundle_info() {
+  $bundles['entity_test_enhanced']['default']['label'] = t('Default');
+  $bundles['entity_test_enhanced']['first']['label'] = t('First');
+  $bundles['entity_test_enhanced']['second']['label'] = t('Second');
+  $bundles['entity_test_enhanced_with_owner']['default']['label'] = t('Default');
+  $bundles['entity_test_enhanced_with_owner']['first']['label'] = t('First');
+  $bundles['entity_test_enhanced_with_owner']['second']['label'] = t('Second');
+
+  return $bundles;
+}
diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.permissions.yml
new file mode 100644 (file)
index 0000000..8aa844e
--- /dev/null
@@ -0,0 +1,3 @@
+'view all entity_test_enhanced revisions':
+  title: 'View all entity_test_enhanced revisions'
+  'restrict access': TRUE
diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.services.yml b/web/modules/contrib/entity/tests/modules/entity_module_test/entity_module_test.services.yml
new file mode 100644 (file)
index 0000000..60f953c
--- /dev/null
@@ -0,0 +1,5 @@
+services:
+  entity_module_test.query_access_subscriber:
+    class: Drupal\entity_module_test\EventSubscriber\QueryAccessSubscriber
+    tags:
+      - { name: event_subscriber }
diff --git a/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php b/web/modules/contrib/entity/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
new file mode 100644 (file)
index 0000000..f06a92d
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\entity_module_test\Entity;
+
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityPublishedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity\Revision\RevisionableContentEntityBase;
+
+/**
+ * Provides a test entity which uses all the capabilities of entity module.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_enhanced",
+ *   label = @Translation("Enhanced entity"),
+ *   label_collection = @Translation("Enhanced entities"),
+ *   label_singular = @Translation("enhanced entity"),
+ *   label_plural = @Translation("enhanced entities"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count enhanced entity",
+ *     plural = "@count enhanced entities",
+ *   ),
+ *   handlers = {
+ *     "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "access" = "\Drupal\entity\EntityAccessControlHandler",
+ *     "query_access" = "\Drupal\entity\QueryAccess\QueryAccessHandler",
+ *     "permission_provider" = "\Drupal\entity\EntityPermissionProvider",
+ *     "form" = {
+ *       "add" = "\Drupal\entity\Form\RevisionableContentEntityForm",
+ *       "edit" = "\Drupal\entity\Form\RevisionableContentEntityForm",
+ *       "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
+ *     },
+ *     "route_provider" = {
+ *       "html" = "\Drupal\entity\Routing\DefaultHtmlRouteProvider",
+ *       "revision" = "\Drupal\entity\Routing\RevisionRouteProvider",
+ *       "delete-multiple" = "\Drupal\entity\Routing\DeleteMultipleRouteProvider",
+ *     },
+ *     "local_action_provider" = {
+ *       "collection" = "\Drupal\entity\Menu\EntityCollectionLocalActionProvider",
+ *     },
+ *     "list_builder" = "\Drupal\Core\Entity\EntityListBuilder",
+ *     "views_data" = "\Drupal\views\EntityViewsData",
+ *   },
+ *   base_table = "entity_test_enhanced",
+ *   data_table = "entity_test_enhanced_field_data",
+ *   revision_table = "entity_test_enhanced_revision",
+ *   revision_data_table = "entity_test_enhanced_field_revision",
+ *   translatable = TRUE,
+ *   revisionable = TRUE,
+ *   admin_permission = "administer entity_test_enhanced",
+ *   permission_granularity = "bundle",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "bundle" = "type",
+ *     "revision" = "vid",
+ *     "langcode" = "langcode",
+ *     "label" = "name",
+ *     "published" = "status",
+ *   },
+ *   links = {
+ *     "add-page" = "/entity_test_enhanced/add",
+ *     "add-form" = "/entity_test_enhanced/add/{type}",
+ *     "edit-form" = "/entity_test_enhanced/{entity_test_enhanced}/edit",
+ *     "canonical" = "/entity_test_enhanced/{entity_test_enhanced}",
+ *     "collection" = "/entity_test_enhanced",
+ *     "delete-multiple-form" = "/entity_test_enhanced/delete",
+ *     "revision" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/view",
+ *     "revision-revert-form" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/revert",
+ *     "version-history" = "/entity_test_enhanced/{entity_test_enhanced}/revisions",
+ *   },
+ * )
+ */
+class EnhancedEntity extends RevisionableContentEntityBase implements EntityPublishedInterface {
+
+  use EntityPublishedTrait;
+
+  /**
+   * {@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,
+      ]);
+
+    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 (file)
index 0000000..214fb21
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+namespace Drupal\entity_module_test\Entity;
+
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityPublishedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity\Revision\RevisionableContentEntityBase;
+use Drupal\user\EntityOwnerInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Provides a test entity which uses all the capabilities of entity module.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_enhanced_with_owner",
+ *   label = @Translation("Enhanced entity with owner"),
+ *   label_collection = @Translation("Enhanced entities with owner"),
+ *   label_singular = @Translation("enhanced entity with owner"),
+ *   label_plural = @Translation("enhanced entities with owner"),
+ *   label_count = @PluralTranslation(
+ *     singular = "@count enhanced entity with owner",
+ *     plural = "@count enhanced entities with owner",
+ *   ),
+ *   handlers = {
+ *     "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "access" = "\Drupal\entity\UncacheableEntityAccessControlHandler",
+ *     "query_access" = "\Drupal\entity\QueryAccess\UncacheableQueryAccessHandler",
+ *     "permission_provider" = "\Drupal\entity\UncacheableEntityPermissionProvider",
+ *     "form" = {
+ *       "add" = "\Drupal\entity\Form\RevisionableContentEntityForm",
+ *       "edit" = "\Drupal\entity\Form\RevisionableContentEntityForm",
+ *       "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
+ *     },
+ *     "route_provider" = {
+ *       "html" = "\Drupal\entity\Routing\DefaultHtmlRouteProvider",
+ *       "revision" = "\Drupal\entity\Routing\RevisionRouteProvider",
+ *       "delete-multiple" = "\Drupal\entity\Routing\DeleteMultipleRouteProvider",
+ *     },
+ *     "local_action_provider" = {
+ *       "collection" = "\Drupal\entity\Menu\EntityCollectionLocalActionProvider",
+ *     },
+ *     "list_builder" = "\Drupal\Core\Entity\EntityListBuilder",
+ *     "views_data" = "\Drupal\views\EntityViewsData",
+ *   },
+ *   base_table = "entity_test_enhanced_with_owner",
+ *   data_table = "entity_test_enhanced_with_owner_field_data",
+ *   revision_table = "entity_test_enhanced_with_owner_revision",
+ *   revision_data_table = "entity_test_enhanced_with_owner_field_revision",
+ *   translatable = TRUE,
+ *   revisionable = TRUE,
+ *   admin_permission = "administer entity_test_enhanced_with_owner",
+ *   permission_granularity = "bundle",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "bundle" = "type",
+ *     "revision" = "vid",
+ *     "langcode" = "langcode",
+ *     "label" = "name",
+ *     "uid" = "user_id",
+ *     "published" = "status",
+ *   },
+ *   links = {
+ *     "add-page" = "/entity_test_enhanced_with_owner/add",
+ *     "add-form" = "/entity_test_enhanced_with_owner/add/{type}",
+ *     "edit-form" = "/entity_test_enhanced_with_owner/{entity_test_enhanced_with_owner}/edit",
+ *     "canonical" = "/entity_test_enhanced_with_owner/{entity_test_enhanced_with_owner}",
+ *     "collection" = "/entity_test_enhanced_with_owner",
+ *   },
+ * )
+ */
+class EnhancedEntityWithOwner extends RevisionableContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface {
+
+  use EntityPublishedTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->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 (file)
index 0000000..5f7ffb6
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\entity_module_test\EventSubscriber;
+
+use Drupal\entity\QueryAccess\ConditionGroup;
+use Drupal\entity\QueryAccess\QueryAccessEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class QueryAccessSubscriber implements EventSubscriberInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      'entity.query_access.entity_test_enhanced' => '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 (file)
index 0000000..2e7772b
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\Tests\entity\Functional;
+
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\Tests\block\Traits\BlockCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the collection route access check.
+ *
+ * @group entity
+ *
+ * @runTestsInSeparateProcesses
+ *
+ * @preserveGlobalState disabled
+ */
+class CollectionRouteAccessTest extends BrowserTestBase {
+
+  use BlockCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['entity_module_test', 'user', 'entity', 'block'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..aaf70c6
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Tests\entity\Functional;
+
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the delete multiple confirmation form.
+ *
+ * @group entity
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class DeleteMultipleFormTest extends BrowserTestBase {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['entity_module_test', 'user', 'entity'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..ca4e119
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\Tests\entity\Functional\Menu;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests that entity local actions are generated correctly.
+ *
+ * @group entity
+ *
+ * @runTestsInSeparateProcesses
+ *
+ * @preserveGlobalState disabled
+ */
+class EntityLocalActionTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['block', 'entity', 'entity_module_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..c40a830
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\Tests\entity\Functional;
+
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\Tests\block\Traits\BlockCreationTrait;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the revision route access check.
+ *
+ * @group entity
+ *
+ * @runTestsInSeparateProcesses
+ *
+ * @preserveGlobalState disabled
+ */
+class RevisionRouteAccessTest extends BrowserTestBase {
+
+  use BlockCreationTrait;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['entity_module_test', 'user', 'entity', 'block'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..eeeb923
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel;
+
+use Drupal\entity_module_bundle_plugin_test\Entity\EntityTestBundlePlugin;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the bundle plugin API.
+ *
+ * @group entity
+ */
+class BundlePluginTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'system',
+    'entity',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..1f2f35a
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel;
+
+use Drupal\entity\Plugin\Action\DeleteAction;
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\system\Entity\Action;
+use Drupal\user\Entity\User;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the delete entity action.
+ *
+ * @group entity
+ */
+class DeleteActionTest extends KernelTestBase {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'action', 'node', 'entity_module_test', 'entity', 'user', 'system',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..34dc09c
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel\QueryAccess;
+
+use Drupal\entity\QueryAccess\Condition;
+use Drupal\entity\QueryAccess\ConditionGroup;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the condition group class.
+ *
+ * ConditionGroup uses \Drupal\Core\Cache\Cache internally, which makes it
+ * impossible to use a unit test (due to Cache accessing the global container).
+ *
+ * @coversDefaultClass \Drupal\entity\QueryAccess\ConditionGroup
+ * @group entity
+ */
+class ConditionGroupTest extends KernelTestBase {
+
+  /**
+   * ::covers getConjunction
+   * ::covers addCondition
+   * ::covers getConditions
+   * ::covers count.
+   */
+  public function testGetters() {
+    $condition_group = new ConditionGroup();
+    $condition_group->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 (file)
index 0000000..8ecef3b
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel\QueryAccess;
+
+use Drupal\entity\QueryAccess\QueryAccessHandler;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+
+/**
+ * Tests the query access event.
+ *
+ * @group entity
+ */
+class QueryAccessEventTest extends EntityKernelTestBase {
+
+  /**
+   * The query access handler.
+   *
+   * @var \Drupal\entity\QueryAccess\QueryAccessHandler
+   */
+  protected $handler;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity',
+    'entity_module_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..6ac3e6c
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel\QueryAccess;
+
+use Drupal\entity\QueryAccess\Condition;
+use Drupal\entity\QueryAccess\QueryAccessHandler;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+
+/**
+ * Tests the query access handler.
+ *
+ * Uses the "entity_test_enhanced" entity type, which has no owner.
+ * UncacheableQueryAccessHandlerTest uses the "entity_test_enhanced_with_owner"
+ * entity type, which has an owner. This ensures both sides (owner and
+ * no owner) are covered.
+ *
+ * @coversDefaultClass \Drupal\entity\QueryAccess\QueryAccessHandler
+ * @group entity
+ */
+class QueryAccessHandlerTest extends EntityKernelTestBase {
+
+  /**
+   * The query access handler.
+   *
+   * @var \Drupal\entity\QueryAccess\QueryAccessHandler
+   */
+  protected $handler;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity',
+    'entity_module_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..068e62a
--- /dev/null
@@ -0,0 +1,343 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel\QueryAccess;
+
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\views\Tests\ViewResultAssertionTrait;
+use Drupal\views\Views;
+
+/**
+ * Test query access filtering for EntityQuery and Views.
+ *
+ * @group entity
+ *
+ * @see \Drupal\entity\QueryAccess\QueryAccessHandler
+ * @see \Drupal\entity\QueryAccess\EntityQueryAlter
+ * @see \Drupal\entity\QueryAccess\ViewsQueryAlter
+ */
+class QueryAccessTest extends EntityKernelTestBase {
+
+  use ViewResultAssertionTrait;
+
+  /**
+   * The test entities.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityInterface[]
+   */
+  protected $entities;
+
+  /**
+   * The entity_test_enhanced storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity',
+    'entity_module_test',
+    'user',
+    'views',
+    'system',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..761a1f7
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel\QueryAccess;
+
+use Drupal\entity\QueryAccess\Condition;
+use Drupal\entity\QueryAccess\ConditionGroup;
+use Drupal\entity\QueryAccess\UncacheableQueryAccessHandler;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+
+/**
+ * Tests the uncacheable query access handler.
+ *
+ * Uses the "entity_test_enhanced_with_owner" entity type, which has an owner.
+ * QueryAccessHandlerTest uses the "entity_test_enhanced" entity type, which
+ * has no owner. This ensures both sides (owner and no owner) are covered.
+ *
+ * @coversDefaultClass \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
+ * @group entity
+ */
+class UncacheableQueryAccessHandlerTest extends EntityKernelTestBase {
+
+  /**
+   * The query access handler.
+   *
+   * @var \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
+   */
+  protected $handler;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity',
+    'entity_module_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..bcd0d35
--- /dev/null
@@ -0,0 +1,511 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel\QueryAccess;
+
+use Drupal\entity_module_test\Entity\EnhancedEntityWithOwner;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\views\Tests\ViewResultAssertionTrait;
+use Drupal\views\Views;
+
+/**
+ * Test uncacheable query access filtering for EntityQuery and Views.
+ *
+ * @group entity
+ *
+ * @see \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
+ * @see \Drupal\entity\QueryAccess\EntityQueryAlter
+ * @see \Drupal\entity\QueryAccess\ViewsQueryAlter
+ */
+class UncacheableQueryAccessTest extends EntityKernelTestBase {
+
+  use ViewResultAssertionTrait;
+
+  /**
+   * The test entities.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityInterface[]
+   */
+  protected $entities;
+
+  /**
+   * The entity_test_enhanced storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity',
+    'entity_module_test',
+    'user',
+    'views',
+    'system',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..8d70bd2
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel;
+
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\Entity\User;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @group entity
+ */
+class RevisionBasicUITest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_module_test', 'system', 'user', 'entity'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..bc28c24
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Tests some integration of the revision overview:
+ *
+ * - Are the routes added properly.
+ * - Are the local tasks added properly.
+ *
+ * @group entity
+ */
+class RevisionOverviewIntegrationTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'entity_module_test', 'entity', 'user', 'system'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->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 (file)
index 0000000..0126d44
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\Tests\entity\Unit;
+
+use Drupal\Core\Cache\Context\CacheContextsManager;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\entity\BundleEntityAccessControlHandler;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\entity\BundleEntityAccessControlHandler
+ * @group entity
+ */
+class BundleEntityAccessControlHandlerTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $module_handler = $this->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 (file)
index 0000000..dbae5dc
--- /dev/null
@@ -0,0 +1,249 @@
+<?php
+
+namespace Drupal\Tests\entity\Unit;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\Context\CacheContextsManager;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\entity\EntityAccessControlHandler;
+use Drupal\entity\EntityPermissionProvider;
+use Drupal\Tests\UnitTestCase;
+use Drupal\user\EntityOwnerInterface;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\entity\EntityAccessControlHandler
+ * @group entity
+ */
+class EntityAccessControlHandlerTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $module_handler = $this->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 (file)
index 0000000..9bb8842
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+
+namespace Drupal\Tests\entity\Unit;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\entity\EntityPermissionProvider;
+use Drupal\Tests\UnitTestCase;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * @coversDefaultClass \Drupal\entity\EntityPermissionProvider
+ * @group entity
+ */
+class EntityPermissionProviderTest extends UnitTestCase {
+
+  /**
+   * The entity permission provider.
+   *
+   * @var \Drupal\entity\EntityPermissionProviderInterface
+   */
+  protected $permissionProvider;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity_type_bundle_info = $this->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 (file)
index 0000000..6d73aeb
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\entity\Unit\QueryAccess;
+
+use Drupal\entity\QueryAccess\Condition;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\entity\QueryAccess\Condition
+ * @group entity
+ */
+class ConditionTest extends UnitTestCase {
+
+  /**
+   * ::covers __construct.
+   *
+   * @expectedException \InvalidArgumentException
+   * @expectedExceptionMessage Unrecognized operator "INVALID".
+   */
+  public function testInvalidOperator() {
+    $condition = new Condition('uid', '1', 'INVALID');
+  }
+
+  /**
+   * ::covers getField
+   * ::covers getValue
+   * ::covers getOperator
+   * ::covers __toString.
+   */
+  public function testGetters() {
+    $condition = new Condition('uid', '2');
+    $this->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 (file)
index 0000000..1cc8467
--- /dev/null
@@ -0,0 +1,253 @@
+<?php
+
+namespace Drupal\Tests\entity\Unit;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\Context\CacheContextsManager;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\entity\UncacheableEntityAccessControlHandler;
+use Drupal\entity\UncacheableEntityPermissionProvider;
+use Drupal\Tests\UnitTestCase;
+use Drupal\user\EntityOwnerInterface;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\entity\UncacheableEntityAccessControlHandler
+ * @group entity
+ */
+class UncacheableEntityAccessControlHandlerTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $module_handler = $this->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 (file)
index 0000000..45cf0fd
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+
+namespace Drupal\Tests\entity\Unit;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\entity\UncacheableEntityPermissionProvider;
+use Drupal\Tests\UnitTestCase;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * @coversDefaultClass \Drupal\entity\UncacheableEntityPermissionProvider
+ * @group entity
+ */
+class UncacheableEntityPermissionProviderTest extends UnitTestCase {
+
+  /**
+   * The entity permission provider.
+   *
+   * @var \Drupal\entity\EntityPermissionProviderInterface
+   */
+  protected $permissionProvider;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity_type_bundle_info = $this->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 (file)
index 0000000..d159169
--- /dev/null
@@ -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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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.
+
+  <signature of Ty Coon>, 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 (file)
index 0000000..75a216a
--- /dev/null
@@ -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 (file)
index 0000000..afbad63
--- /dev/null
@@ -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 (file)
index 0000000..05a3b27
--- /dev/null
@@ -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 (file)
index 0000000..cead27b
--- /dev/null
@@ -0,0 +1,282 @@
+<?php
+
+/**
+ * @file
+ * Provides a field that can reference other entities.
+ */
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\TranslatableRevisionableStorageInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_help().
+ */
+function entity_reference_revisions_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.entity_reference_revisions':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . 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 <a href=":er_do">the online documentation for the Entity Reference Revisions module</a> and the <a href=":field_help">Field module help page</a>.', [':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':er_do' => 'https://drupal.org/documentation/modules/entity_reference_revisions']) . '</p>';
+      $output .= '<h3>' . t('Uses') . '</h3>';
+      $output .= '<dl>';
+      $output .= '<dt>' . t('Managing and displaying entity reference fields') . '</dt>';
+      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the entity reference field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => Url::fromRoute('help.page', ['name' => 'field_ui'])->toString()]) . '</dd>';
+      $output .= '<dt>' . t('Selecting reference type') . '</dt>';
+      $output .= '<dd>' . t('In the field settings you can select which entity type you want to create a reference to.') . '</dd>';
+      $output .= '<dt>' . t('Filtering and sorting reference fields') . '</dt>';
+      $output .= '<dd>' . 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.') . '</dd>';
+      $output .= '<dt>' . t('Displaying a reference') . '</dt>';
+      $output .= '<dd>' . 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.') . '</dd>';
+      $output .= '</dl>';
+      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 (file)
index 0000000..9d351b3
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Provides views data for the entity_reference_revisions module.
+ */
+
+use Drupal\field\FieldStorageConfigInterface;
+
+/**
+ * Implements hook_field_views_data().
+ */
+function entity_reference_revisions_field_views_data(FieldStorageConfigInterface $field_storage) {
+  $data = views_field_default_views_data($field_storage);
+  $entity_manager = \Drupal::entityTypeManager();
+  foreach ($data as $table_name => $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 (file)
index 0000000..1513aa3
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\entity_reference_revisions;
+
+/**
+ * Allows an entity to define whether it needs to be saved.
+ */
+interface EntityNeedsSaveInterface {
+
+  /**
+   * Checks whether the entity needs to be saved.
+   *
+   * @return bool
+   *   TRUE if the entity needs to be saved.
+   */
+  public function needsSave();
+}
diff --git a/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php b/web/modules/contrib/entity_reference_revisions/src/EntityNeedsSaveTrait.php
new file mode 100644 (file)
index 0000000..3304010
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\entity_reference_revisions;
+use Drupal\Core\Entity\EntityStorageInterface;
+
+/**
+ * Trait for EntityNeedsSaveInterface.
+ */
+trait EntityNeedsSaveTrait {
+
+  /**
+   * Whether the entity needs to be saved or not.
+   *
+   * @var bool
+   */
+  protected $needsSave = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function needsSave() {
+    return $this->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 (file)
index 0000000..983e7be
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\entity_reference_revisions;
+
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldItemListTranslationChangesInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemList;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+
+/**
+ * Defines a item list class for entity reference fields.
+ */
+class EntityReferenceRevisionsFieldItemList extends EntityReferenceFieldItemList implements EntityReferenceFieldItemListInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function referencedEntities() {
+    if (empty($this->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 (file)
index 0000000..65db785
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\entity_reference_revisions;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceProviderBase;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Service Provider for Entity Reference Revisions.
+ */
+class EntityReferenceRevisionsServiceProvider extends ServiceProviderBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alter(ContainerBuilder $container) {
+    $modules = $container->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 (file)
index 0000000..5c63cf2
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Normalizer;
+
+use Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem;
+use Drupal\hal\Normalizer\EntityReferenceItemNormalizer;
+
+/**
+ * Defines a class for normalizing EntityReferenceRevisionItems.
+ */
+class EntityReferenceRevisionItemNormalizer extends EntityReferenceItemNormalizer {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = EntityReferenceRevisionsItem::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function constructValue($data, $context) {
+    $value = parent::constructValue($data, $context);
+    if ($value) {
+      $value['target_revision_id'] = $data['target_revision_id'];
+    }
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($field_item, $format = NULL, array $context = array()) {
+    $data = parent::normalize($field_item, $format, $context);
+    $field_name = $field_item->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 (file)
index 0000000..b24b6b1
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\DataType;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Plugin\DataType\EntityReference;
+
+/**
+ * Defines an 'entity_reference_revisions' data type.
+ *
+ * This serves as 'entity' property of entity reference field items and gets
+ * its value set from the parent, i.e. LanguageItem.
+ *
+ * The plain value of this reference is the entity object, i.e. an instance of
+ * \Drupal\Core\Entity\EntityInterface. For setting the value the entity object
+ * or the entity ID may be passed.
+ *
+ * Note that the definition of the referenced entity's type is required, whereas
+ * defining referencable entity bundle(s) is optional. A reference defining the
+ * type and bundle of the referenced entity can be created as following:
+ * @code
+ * $definition = \Drupal\Core\Entity\EntityDefinition::create($entity_type)
+ *   ->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 (file)
index 0000000..461fd53
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\DataType;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
+
+/**
+ * Defines the "entity" data type.
+ *
+ * Instances of this class wrap entity objects and allow to deal with entities
+ * based upon the Typed Data API.
+ *
+ * In addition to the "entity" data type, this exposes derived
+ * "entity:$entity_type" and "entity:$entity_type:$bundle" data types.
+ *
+ * @DataType(
+ *   id = "entity_revision",
+ *   label = @Translation("Entity Revision"),
+ *   description = @Translation("All kind of entities with revision information, e.g. nodes, comments or users."),
+ *   deriver = "\Drupal\Core\Entity\Plugin\DataType\Deriver\EntityDeriver",
+ *   definition_class = "\Drupal\entity_reference_revisions\TypedData\EntityRevisionDataDefinition"
+ * )
+ */
+class EntityRevisionsAdapter extends EntityAdapter implements \IteratorAggregate, ComplexDataInterface {
+
+}
diff --git a/web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php b/web/modules/contrib/entity_reference_revisions/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php
new file mode 100644 (file)
index 0000000..bb7d0c2
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\Derivative;
+
+use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions;
+use Drupal\migrate\Plugin\Derivative\MigrateEntityRevision;
+
+/**
+ * Class MigrateEntityReferenceRevisions
+ */
+class MigrateEntityReferenceRevisions extends MigrateEntityRevision  {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($basePluginDefinition) {
+    foreach ($this->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 (file)
index 0000000..5d23ec4
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'entity reference rendered entity' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "entity_reference_revisions_entity_view",
+ *   label = @Translation("Rendered entity"),
+ *   description = @Translation("Display the referenced entities rendered by entity_view()."),
+ *   field_types = {
+ *     "entity_reference_revisions"
+ *   }
+ * )
+ */
+class EntityReferenceRevisionsEntityFormatter extends EntityReferenceRevisionsFormatterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The logger factory.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
+   */
+  protected $loggerFactory;
+
+  /**
+   * The entity display repository.
+   *
+   * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
+   */
+  protected $entityDisplayRepository;
+
+  /**
+   * Constructs a StringFormatter instance.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Any third party settings settings.
+   * @param LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
+   * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
+   *   The entity display repository.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, LoggerChannelFactoryInterface $logger_factory, EntityDisplayRepositoryInterface $entity_display_repository) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+    $this->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 (file)
index 0000000..e098f52
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
+
+/**
+ * Parent plugin for entity reference formatters.
+ */
+abstract class EntityReferenceRevisionsFormatterBase extends EntityReferenceFormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareView(array $entities_items) {
+    // Entity revision loading currently has no static/persistent cache and no
+    // multiload. As entity reference checks _loaded, while we don't want to
+    // indicate a loaded entity, when there is none, as it could cause errors,
+    // we actually load the entity and set the flag.
+    foreach ($entities_items as $items) {
+      foreach ($items as $item) {
+
+        if ($item->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 (file)
index 0000000..24b3224
--- /dev/null
@@ -0,0 +1,512 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType;
+
+use Drupal\Component\Utility\Random;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\TranslatableRevisionableInterface;
+use Drupal\Core\Entity\TypedData\EntityDataDefinition;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\TypedData\DataReferenceDefinition;
+use Drupal\Core\TypedData\DataReferenceTargetDefinition;
+use Drupal\Core\TypedData\OptionsProviderInterface;
+use Drupal\entity_reference_revisions\EntityNeedsSaveInterface;
+
+/**
+ * Defines the 'entity_reference_revisions' entity field type.
+ *
+ * Supported settings (below the definition's 'settings' key) are:
+ * - target_type: The entity type to reference. Required.
+ * - target_bundle: (optional): If set, restricts the entity bundles which may
+ *   may be referenced. May be set to an single bundle, or to an array of
+ *   allowed bundles.
+ *
+ * @FieldType(
+ *   id = "entity_reference_revisions",
+ *   label = @Translation("Entity reference revisions"),
+ *   description = @Translation("An entity field containing an entity reference to a specific revision."),
+ *   category = @Translation("Reference revisions"),
+ *   no_ui = FALSE,
+ *   class = "\Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem",
+ *   list_class = "\Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList",
+ *   default_formatter = "entity_reference_revisions_entity_view",
+ *   default_widget = "entity_reference_revisions_autocomplete"
+ * )
+ */
+class EntityReferenceRevisionsItem extends EntityReferenceItem implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+
+    $entity_types = \Drupal::entityTypeManager()->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 (file)
index 0000000..faad139
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Entity\Entity;
+use Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Plugin implementation of the 'entity_reference_autocomplete' widget.
+ *
+ * @FieldWidget(
+ *   id = "entity_reference_revisions_autocomplete",
+ *   label = @Translation("Autocomplete"),
+ *   description = @Translation("An autocomplete text field."),
+ *   field_types = {
+ *     "entity_reference_revisions"
+ *   }
+ * )
+ */
+class EntityReferenceRevisionsAutocompleteWidget extends EntityReferenceAutocompleteWidget {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    $entity_type = $this->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 (file)
index 0000000..0df0507
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\diff\Field;
+
+use Drupal\diff\FieldDiffBuilderBase;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\diff\FieldReferenceInterface;
+
+/**
+ * This plugins offers the possibility to compare ERR fields.
+ *
+ * @FieldDiffBuilder(
+ *   id = "entity_reference_revisions_field_diff_builder",
+ *   label = @Translation("Field Diff for Paragraphs"),
+ *   field_types = {
+ *     "entity_reference_revisions"
+ *   },
+ * )
+ */
+class EntityReferenceRevisionsFieldDiffBuilder extends FieldDiffBuilderBase implements FieldReferenceInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(FieldItemListInterface $field_items) {
+    $result_text = array();
+    $item_counter = 0;
+    /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item */
+    foreach ($field_items as $field_key => $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 (file)
index 0000000..75644b2
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\migrate\destination;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\Plugin\migrate\destination\EntityRevision;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Row;
+
+/**
+ * Provides entity_reference_revisions destination plugin.
+ *
+ * Available configuration keys:
+ * - new_revisions: (optional) Flag to indicate if a new revision should be
+ *   created instead of updating a previous default record. Only applicable when
+ *   providing an entity id without a revision_id.
+ *
+ * @MigrateDestination(
+ *   id = "entity_reference_revisions",
+ *   deriver = "Drupal\entity_reference_revisions\Plugin\Derivative\MigrateEntityReferenceRevisions"
+ * )
+ */
+class EntityReferenceRevisions extends EntityRevision implements ConfigurablePluginInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager);
+    $this->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 (file)
index 0000000..3c73856
--- /dev/null
@@ -0,0 +1,176 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\views\display;
+
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+
+/**
+ * The plugin that handles an EntityReferenceRevisions display.
+ *
+ * "entity_reference_revisions_display" is a custom property, used with
+ * \Drupal\views\Views::getApplicableViews() to retrieve all views with a
+ * 'Entity Reference Revisions' display.
+ *
+ * @ingroup views_display_plugins
+ *
+ * @ViewsDisplay(
+ *   id = "entity_reference_revisions",
+ *   title = @Translation("Entity Reference Revisions"),
+ *   admin = @Translation("Entity Reference Revisions Source"),
+ *   help = @Translation("Selects referenceable entities for an entity reference revisions field."),
+ *   theme = "views_view",
+ *   register_theme = FALSE,
+ *   uses_menu_links = FALSE,
+ *   entity_reference_revisions_display = TRUE
+ * )
+ */
+class EntityReferenceRevisions extends DisplayPluginBase {
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$useAJAX.
+   */
+  protected $usesAJAX = FALSE;
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesPager.
+   */
+  protected $usesPager = FALSE;
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::$usesAttachments.
+   */
+  protected $usesAttachments = FALSE;
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::defineOptions().
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+
+    // Force the style plugin to 'entity_reference_style' and the row plugin to
+    // 'fields'.
+    $options['style']['contains']['type'] = array('default' => '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 (file)
index 0000000..fe25de2
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\views\row;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\views\row\Fields;
+
+/**
+ * EntityReferenceRevisions row plugin.
+ *
+ * @ingroup views_row_plugins
+ *
+ * @ViewsRow(
+ *   id = "entity_reference_revisions",
+ *   title = @Translation("Entity Reference inline fields"),
+ *   help = @Translation("Displays the fields with an optional template."),
+ *   theme = "views_view_fields",
+ *   register_theme = FALSE,
+ *   display_types = {"entity_reference"}
+ * )
+ */
+class EntityReferenceRevisions extends Fields {
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\row\Fields::defineOptions().
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['separator'] = array('default' => '-');
+
+    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'] .= '<br />' . $this->t("<strong>Note:</strong> 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 (file)
index 0000000..376491e
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Plugin\views\style;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\views\style\StylePluginBase;
+
+/**
+ * EntityReferenceRevisions style plugin.
+ *
+ * @ingroup views_style_plugins
+ *
+ * @ViewsStyle(
+ *   id = "entity_reference_revisions",
+ *   title = @Translation("Entity Reference Revisions list"),
+ *   help = @Translation("Returns results as a PHP array of labels and rendered rows."),
+ *   theme = "views_view_unformatted",
+ *   register_theme = FALSE,
+ *   display_types = {"entity_reference_revisions"}
+ * )
+ */
+class EntityReferenceRevisions extends StylePluginBase {
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\style\StylePluginBase::usesRowPlugin.
+   */
+  protected $usesRowPlugin = TRUE;
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\style\StylePluginBase::usesFields.
+   */
+  protected $usesFields = TRUE;
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\style\StylePluginBase::usesGrouping.
+   */
+  protected $usesGrouping = FALSE;
+
+  /**
+   * Overrides \Drupal\views\Plugin\views\style\StylePluginBase\StylePluginBase::defineOptions().
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['search_fields'] = array('default' => 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 (file)
index 0000000..cc7caed
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Tests;
+
+use Drupal\field_ui\Tests\FieldUiTestTrait;
+use Drupal\node\Entity\Node;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the entity_reference_revisions configuration.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsAdminTest extends WebTestBase {
+
+  use FieldUiTestTrait;
+  use EntityReferenceRevisionsCoreVersionUiTestTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'field_ui',
+    'block',
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Create paragraphs and article content types.
+    $this->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 (file)
index 0000000..a80c615
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Tests;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\Component\Utility\Html;
+use Drupal\field_ui\Tests\FieldUiTestTrait;
+use Drupal\node\Entity\Node;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the entity_reference_revisions autocomplete.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsAutocompleteTest extends WebTestBase {
+
+  use FieldUiTestTrait;
+  use EntityReferenceRevisionsCoreVersionUiTestTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'block_content',
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'field_ui',
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Create article content type.
+    $this->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 (file)
index 0000000..596d941
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Tests;
+
+/**
+ * Provides helper methods for Drupal 8.3.x and 8.4.x versions.
+ */
+trait EntityReferenceRevisionsCoreVersionUiTestTrait {
+
+  /**
+   * An adapter for 8.3 > 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 (file)
index 0000000..d4f236a
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Tests;
+
+use Drupal\field_ui\Tests\FieldUiTestTrait;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the entity_reference_revisions diff plugin.
+ *
+ * @group entity_reference_revisions
+ *
+ * @dependencies diff
+ */
+class EntityReferenceRevisionsDiffTest extends WebTestBase {
+
+  use FieldUiTestTrait;
+  use EntityReferenceRevisionsCoreVersionUiTestTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'block_content',
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'field_ui',
+    'diff',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Create article content type.
+    $this->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 (file)
index 0000000..ea5bcaf
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\Tests;
+
+use Drupal\field_ui\Tests\FieldUiTestTrait;
+use Drupal\node\Entity\Node;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the entity_reference_revisions configuration.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsNormalizerTest extends WebTestBase {
+
+  use FieldUiTestTrait;
+  use EntityReferenceRevisionsCoreVersionUiTestTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'field_ui',
+    'block',
+    'hal',
+    'serialization',
+    'rest',
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Create paragraphs and article content types.
+    $this->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 (file)
index 0000000..7ffac7d
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\entity_reference_revisions\TypedData;
+
+use Drupal\Core\Entity\TypedData\EntityDataDefinition;
+use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
+
+/**
+ * A typed data definition class for describing entities.
+ */
+class EntityRevisionDataDefinition extends EntityDataDefinition implements EntityDataDefinitionInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createFromDataType($data_type) {
+    $parts = explode(':', $data_type);
+    if ($parts[0] != 'entity_revision') {
+      throw new \InvalidArgumentException('Data type must be in the form of "entity_revision:ENTITY_TYPE:BUNDLE."');
+    }
+    $definition = static::create();
+    // Set the passed entity type and bundle.
+    if (isset($parts[1])) {
+      $definition->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 (file)
index 0000000..2172d62
--- /dev/null
@@ -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 (file)
index 0000000..c9be8a0
--- /dev/null
@@ -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 (file)
index 0000000..c74765f
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\entity_composite_relationship_test\Entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity_reference_revisions\EntityNeedsSaveInterface;
+use Drupal\entity_reference_revisions\EntityNeedsSaveTrait;
+use Drupal\entity_test\Entity\EntityTestMulRev;
+
+/**
+ * Defines the test entity class.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_composite",
+ *   label = @Translation("Test entity - composite relationship"),
+ *   base_table = "entity_test_composite",
+ *   revision_table = "entity_test_composite_revision",
+ *   data_table = "entity_test_composite_field_data",
+ *   revision_data_table = "entity_test_composite_field_revision",
+ *   content_translation_ui_skip = TRUE,
+ *   translatable = TRUE,
+ *   entity_revision_parent_type_field = "parent_type",
+ *   entity_revision_parent_id_field = "parent_id",
+ *   entity_revision_parent_field_name_field = "parent_field_name",
+ *   admin_permission = "administer entity_test composite relationship",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "uuid" = "uuid",
+ *     "revision" = "revision_id",
+ *     "bundle" = "type",
+ *     "label" = "name",
+ *     "langcode" = "langcode",
+ *   }
+ * )
+ */
+class EntityTestCompositeRelationship extends EntityTestMulRev implements EntityNeedsSaveInterface {
+
+  use EntityNeedsSaveTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+    $fields['parent_id'] = BaseFieldDefinition::create('string')
+      ->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 (file)
index 0000000..1509e44
--- /dev/null
@@ -0,0 +1,423 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel;
+
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+/**
+ * Tests the entity_reference_revisions composite relationship.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsCompositeTest extends EntityKernelTestBase {
+
+  use ContentTypeCreationTrait;
+  use NodeCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+    'language'
+  );
+
+  /**
+   * The current database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   *
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $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' => 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 (file)
index 0000000..4e261bf
--- /dev/null
@@ -0,0 +1,351 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel;
+
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+/**
+ * Tests entity_reference_revisions composites with a translatable field.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsCompositeTranslatableFieldTest extends EntityKernelTestBase {
+
+  use ContentTypeCreationTrait;
+  use NodeCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+    'language',
+    'content_translation'
+  );
+
+  /**
+   * The current database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   *
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    ConfigurableLanguage::createFromLangcode('de')->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 (file)
index 0000000..fbe4c14
--- /dev/null
@@ -0,0 +1,459 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+/**
+ * Tests the entity_reference_revisions composite relationship.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsCompositeTranslationTest extends EntityKernelTestBase {
+
+  use ContentTypeCreationTrait;
+  use NodeCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'node',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+    'language',
+    'content_translation'
+  ];
+
+  /**
+   * The current database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   *
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    ConfigurableLanguage::createFromLangcode('de')->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 (file)
index 0000000..430d06e
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel;
+
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\simpletest\UserCreationTrait;
+
+/**
+ * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\Field\FieldFormatter\EntityReferenceRevisionsEntityFormatter
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsFormatterTest extends KernelTestBase {
+
+  use UserCreationTrait;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'node',
+    'user',
+    'system',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Create article content type.
+    $values = ['type' => '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 (file)
index 0000000..2b66a63
--- /dev/null
@@ -0,0 +1,321 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel;
+
+use Drupal\entity_composite_relationship_test\Entity\EntityTestCompositeRelationship;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+
+/**
+ * Tests the entity_reference_revisions NeedsSaveInterface.
+ *
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsSaveTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'node',
+    'user',
+    'system',
+    'field',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Create article content type.
+    $values = ['type' => '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 (file)
index 0000000..e68cf05
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel\Plugin\Derivative;
+
+use Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\migrate\Plugin\MigrationPluginManager;
+use Drupal\migrate\Plugin\MigrateDestinationPluginManager;
+
+/**
+ * Tests the migration deriver.
+ *
+ * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\Derivative\MigrateEntityReferenceRevisions
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsDeriverTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['migrate', 'entity_reference_revisions', 'entity_composite_relationship_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->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 (file)
index 0000000..f6f42fc
--- /dev/null
@@ -0,0 +1,540 @@
+<?php
+
+namespace Drupal\Tests\entity_reference_revisions\Kernel\Plugin\migrate\destination;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\migrate\MigrateExecutable;
+use Drupal\migrate\MigrateMessageInterface;
+use Drupal\migrate\Plugin\Migration;
+use Drupal\node\Entity\NodeType;
+
+/**
+ * Tests the migration destination plugin.
+ *
+ * @coversDefaultClass \Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions
+ * @group entity_reference_revisions
+ */
+class EntityReferenceRevisionsDestinationTest extends KernelTestBase implements MigrateMessageInterface {
+
+  /**
+   * The migration plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigrationPluginManager
+   */
+  protected $migrationPluginManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'migrate',
+    'entity_reference_revisions',
+    'entity_composite_relationship_test',
+    'user',
+    'system',
+    'text',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->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);
+  }
+
+}